Golangのライブラリにおけるpanic
はじめに
最初にGolangを学んだとき、私はJava、PHP、Python、Rubyなどの例外を持つ言語のアクティブなユーザーでした。
大規模なプロジェクトでは例外を使って振る舞いを管理し、親のコールスタックでtry/catch
ブロックを使ってフローを制御することが一般的です。
例外がない言語、あるいは例外を禁止するコードベースでの経験もありました。
例外なしの経験は、コードベースに多くの制限を加えるため、通常はあまり楽しくありませんでした。
さらに、これらのコードベースは多くの場合、複数の値を返す機能をサポートしていなかったため、int
の-1
や参照のnullなどのサポートされていない値を返すことでフロー制御を構築する必要がありました。
制限のため、参照パラメータを使用して制限を処理し、それらを通じて結果を_返す_方法がよく使われていました。
Golang
さあ、初めてGolangに出会いました。
ドキュメントや記事や本によると、例外の代わりにerror
の戻り値を使うべきだとされています。さらに、Golangには例外がまったく(またはほとんど)サポートされていません。
Golangのドキュメントやコードベースを読むと、多くの人がpanic
とrecover
を例外として使用できることに気づきます。これについて多くの記事、投稿、コメントが書かれています。
しかし、Golangコミュニティは常にこれらの考えを批判し、panic
の代わりにerror
の戻り値を使うことを提案しています。さらに、これらの人々はGolangにおけるerror
の扱い方が他の言語の例外よりも優れているという意見を共有しています。
なぜそうされるのかを理解してみましょう。
エラー処理
Golangにおける例外とエラー処理を比較する前に、エラーを処理する主要な方法を見てみましょう。
3つの方法があります:
- 関数からエラーを返す。
- 呼び出しスタックを通じてエラーを投げ、誰かがそれをキャッチするまで。
- プログラムの実行を停止する。
これらの方法は、多くの異なる言語で並行して使用され、どれが他よりも優れているわけではありません。
関数からエラーを返す
この方法は、CやAssemblyでプログラムを学び始めたときに見た最初の方法です。これはエラーを処理するための最も明らかな方法です。なぜなら、エラーの場合には特別な値が返されることに単に合意するだけです。Cの場合はint
の-1
、参照のNULL
です。時には、参照によってエラー値を埋めるerror
パラメータがあることもあります。
この方法の最良の点は、非常にシンプルで理解しやすいことです。これにより、コードを読みやすくし、変数のスコープを確認するために値を印刷したり、デバッガーを行に置いたりしてデバッグしやすくします。
Cの場合は、エラーをどのように扱うのか予測できないため、混乱することがあり、それぞれの関数のドキュメントを確認してエラーの扱いを理解する必要がありました。
GolangやRustなどの「現代」の言語では、特定のerror
型と、それらを使用する方法に関する規則が定義されています。これにより可読性が向上し、関数のインタフェースが単純化されます。
基本的に、それを取り巻くすべての変更は単なる見た目の違いですが、コンセプトは同じです。
上のダイアグラムで、このアプローチを使ってエラーを処理するプログラムの流れを見ることができます。
ダイアグラムの簡単な説明:
main
関数がfunc1
関数を呼び出す。func1
関数がfunc2
関数を呼び出す。func2
関数がエラーを返す。func1
関数はfunc2
関数から返された値を確認し、func1
からのerror
に基づいてエラーを返す。main
関数はfunc1
関数からの返り値を確認し、error
を処理する。main
関数はプログラムの実行を続けることができる。
このアプローチの主なアイデアは、コールスタック内の各関数でエラーを処理する必要があり、コールスタックの任意のレベルでエラーを処理できることです。もし特定のレベルで何をするべきかわからない場合は、エラーを呼び出し元に返し、それに応じて処理させることができます。
長所
- 理解しやすい。
- デバッグしやすい。
- 使用が簡単。
短所
- 場合によってはボイラープレートコードが増える。
- 関数の各呼び出しでエラー処理について考える必要がある。
- コールスタック内のすべての関数でエラーを処理し、必要に応じてそれを呼び出し元に返す必要がある。
- エラーを無視することが単純であり、無視する場合の処理を追加しないことがある。
エラーは取り扱いが必要であり、開発者が常にエラーを意識することを保証するために使われます。
現在のコールスタックを通してエラーを投げ、誰かがキャッチするまで
このアプローチの主なアイデアは、関数からエラーを投げて、それを呼び出しスタックを上って誰かがキャッチするまで続けることです。
この方法は、Java、PHP、Python、Rubyなどの例外を持つ言語でエラーを処理する最も一般的な方法です。
このアプローチを使うことで、中間レベルに追加のコードや知識を加えることなく、コールスタックを通じてerror
を送信できます。これにより、開発が簡素化され、コールスタックのほとんどのレベルでエラーについて考える必要がなくなります。
例外をサポートする言語では、例外をスローするための特別なキーワードと、それをキャッチするための構文があります。
- JavaとPHPの場合は
throw
とtry/catch
ブロックです。 - Pythonの場合は
raise
とtry/except
ブロックです。 - Rubyの場合は
raise
とbegin/rescue
ブロックです。
しかし、すべてに共通する実行フローを制御するアイデアは、例外をスローして、それを処理したいレベルでキャッチするというものです。
Goでは、少し異なります。例外をサポートしていませんが、panic
とrecover
関数があり、それらは重大なエラーを処理するために使われます。
上のダイアグラムで、このアプローチを使ってエラーを処理するプログラムの流れを見ることができます。
ダイアグラムの簡単な説明:
main
関数がfunc1
関数を呼び出す。func1
関数がfunc2
関数を呼び出す。func2
関数がerror
をスローする。main
関数がerror
をキャッチし、それを処理する。main
関数がプログラムの実行を続けることができる。
この方法は、新しいコードを書くのを簡単にし、コールスタックの中間レベルでのボイラープレートコードを削減するために使われます。
長所
- 新しいコードを書くのが簡単。
- コールスタックの中間レベルでゼロのボイラープレートコード。
短所
- エラーがスローされた場所を理解するのが難しい。
- デバッグが困難。
- エラーがどこでキャッチされるか、全くキャッチされるかどうかを理解するのが困難。
- エラーがキャッチされなければ、プログラムは実行中に停止する。
プログラムの実行を停止する
エラーを処理する最も激しい方法は、エラーがある場合にプログラムの実行を止めることです。
ほとんどの言語では、プログラムの実行を止めて呼び出し元にエラーコードを返すexit
関数を使用します。
また、assert
関数を使用して条件を確認し、それが満たされない場合にプログラムの実行を停止させます。
exit
やassert
関数が呼び出された後にプログラムの実行を回復する方法はありません。一方通行のエラー処理は、ビジネスフローに関連しない一部の致命的なエラーに対し、プログラムによって処理されることができない場合に実行を止めるために使われます。
このようなエラーの最も単純な例は、コマンドラインインターフェースでパラメータの値がサポートされていない場合です。この場合には、プログラムの実行を止め、エラーメッセージをユーザーに表示することができます。
上のアプローチを使ってエラーを処理するプログラムの流れを、上のダイアグラムで見てみましょう。このダイアグラムには、プログラムのエントリーポイント1から実行するオペレーティングシステムを表すOSレベルが追加されています。
簡単なダイアグラムの説明:
os
は、通常はmain
関数の呼び出しによってプログラムのエントリーポイントを呼び出します。main
関数がfunc1
関数を呼び出す。func1
関数がfunc2
関数を呼び出す。func2
関数は条件を確認し、プログラムの実行を停止する。os
はプログラムからエラーコードを受け取り、それを処理し、プログラムの実行を停止する。
このアプローチは、ビジネスフローに関連しない致命的なエラーが発生した場合にプログラムの実行を止めるために使われます。
長所
- プログラムの実行を簡単に止める。
- 致命的なエラーの場合、プ
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/aohorodnyk/golang-panics-in-libraries-3dh7