Golangのライブラリにおけるpanic

はじめに

最初にGolangを学んだとき、私はJavaPHPPythonRubyなどの例外を持つ言語のアクティブなユーザーでした。

大規模なプロジェクトでは例外を使って振る舞いを管理し、親のコールスタックでtry/catchブロックを使ってフローを制御することが一般的です。

例外がない言語、あるいは例外を禁止するコードベースでの経験もありました。

例外なしの経験は、コードベースに多くの制限を加えるため、通常はあまり楽しくありませんでした。
さらに、これらのコードベースは多くの場合、複数の値を返す機能をサポートしていなかったため、int-1や参照のnullなどのサポートされていない値を返すことでフロー制御を構築する必要がありました。
制限のため、参照パラメータを使用して制限を処理し、それらを通じて結果を_返す_方法がよく使われていました。

Golang

さあ、初めてGolangに出会いました。
ドキュメントや記事や本によると、例外の代わりにerrorの戻り値を使うべきだとされています。さらに、Golangには例外がまったく(またはほとんど)サポートされていません。

Golangのドキュメントやコードベースを読むと、多くの人がpanicrecoverを例外として使用できることに気づきます。これについて多くの記事、投稿、コメントが書かれています。
しかし、Golangコミュニティは常にこれらの考えを批判し、panicの代わりにerrorの戻り値を使うことを提案しています。さらに、これらの人々はGolangにおけるerrorの扱い方が他の言語の例外よりも優れているという意見を共有しています。

なぜそうされるのかを理解してみましょう。

エラー処理

Golangにおける例外とエラー処理を比較する前に、エラーを処理する主要な方法を見てみましょう。

3つの方法があります:

  • 関数からエラーを返す。
  • 呼び出しスタックを通じてエラーを投げ、誰かがそれをキャッチするまで。
  • プログラムの実行を停止する。

これらの方法は、多くの異なる言語で並行して使用され、どれが他よりも優れているわけではありません。

関数からエラーを返す

この方法は、CAssemblyでプログラムを学び始めたときに見た最初の方法です。これはエラーを処理するための最も明らかな方法です。なぜなら、エラーの場合には特別な値が返されることに単に合意するだけです。Cの場合はint-1、参照のNULLです。時には、参照によってエラー値を埋めるerrorパラメータがあることもあります。

この方法の最良の点は、非常にシンプルで理解しやすいことです。これにより、コードを読みやすくし、変数のスコープを確認するために値を印刷したり、デバッガーを行に置いたりしてデバッグしやすくします。
Cの場合は、エラーをどのように扱うのか予測できないため、混乱することがあり、それぞれの関数のドキュメントを確認してエラーの扱いを理解する必要がありました。

GolangRustなどの「現代」の言語では、特定のerror型と、それらを使用する方法に関する規則が定義されています。これにより可読性が向上し、関数のインタフェースが単純化されます。

基本的に、それを取り巻くすべての変更は単なる見た目の違いですが、コンセプトは同じです。

関数からのエラー返却フローダイアグラム

上のダイアグラムで、このアプローチを使ってエラーを処理するプログラムの流れを見ることができます。
ダイアグラムの簡単な説明:

  • main関数がfunc1関数を呼び出す。
  • func1関数がfunc2関数を呼び出す。
  • func2関数がエラーを返す。
  • func1関数はfunc2関数から返された値を確認し、func1からのerrorに基づいてエラーを返す。
  • main関数はfunc1関数からの返り値を確認し、errorを処理する。
  • main関数はプログラムの実行を続けることができる。

このアプローチの主なアイデアは、コールスタック内の各関数でエラーを処理する必要があり、コールスタックの任意のレベルでエラーを処理できることです。もし特定のレベルで何をするべきかわからない場合は、エラーを呼び出し元に返し、それに応じて処理させることができます。

長所

  • 理解しやすい。
  • デバッグしやすい。
  • 使用が簡単。

短所

  • 場合によってはボイラープレートコードが増える。
  • 関数の各呼び出しでエラー処理について考える必要がある。
  • コールスタック内のすべての関数でエラーを処理し、必要に応じてそれを呼び出し元に返す必要がある。
  • エラーを無視することが単純であり、無視する場合の処理を追加しないことがある。

エラーは取り扱いが必要であり、開発者が常にエラーを意識することを保証するために使われます。

現在のコールスタックを通してエラーを投げ、誰かがキャッチするまで

このアプローチの主なアイデアは、関数からエラーを投げて、それを呼び出しスタックを上って誰かがキャッチするまで続けることです。
この方法は、JavaPHPPythonRubyなどの例外を持つ言語でエラーを処理する最も一般的な方法です。

このアプローチを使うことで、中間レベルに追加のコードや知識を加えることなく、コールスタックを通じてerrorを送信できます。これにより、開発が簡素化され、コールスタックのほとんどのレベルでエラーについて考える必要がなくなります。

例外をサポートする言語では、例外をスローするための特別なキーワードと、それをキャッチするための構文があります。

  • JavaPHPの場合はthrowtry/catchブロックです。
  • Pythonの場合はraisetry/exceptブロックです。
  • Rubyの場合はraisebegin/rescueブロックです。

しかし、すべてに共通する実行フローを制御するアイデアは、例外をスローして、それを処理したいレベルでキャッチするというものです。

Goでは、少し異なります。例外をサポートしていませんが、panicrecover関数があり、それらは重大なエラーを処理するために使われます。

例外をスローするフローダイアグラム

上のダイアグラムで、このアプローチを使ってエラーを処理するプログラムの流れを見ることができます。
ダイアグラムの簡単な説明:

  • main関数がfunc1関数を呼び出す。
  • func1関数がfunc2関数を呼び出す。
  • func2関数がerrorをスローする。
  • main関数がerrorをキャッチし、それを処理する。
  • main関数がプログラムの実行を続けることができる。

この方法は、新しいコードを書くのを簡単にし、コールスタックの中間レベルでのボイラープレートコードを削減するために使われます。

長所

  • 新しいコードを書くのが簡単。
  • コールスタックの中間レベルでゼロのボイラープレートコード。

短所

  • エラーがスローされた場所を理解するのが難しい。
  • デバッグが困難。
  • エラーがどこでキャッチされるか、全くキャッチされるかどうかを理解するのが困難。
  • エラーがキャッチされなければ、プログラムは実行中に停止する。

プログラムの実行を停止する

エラーを処理する最も激しい方法は、エラーがある場合にプログラムの実行を止めることです。
ほとんどの言語では、プログラムの実行を止めて呼び出し元にエラーコードを返すexit関数を使用します。
また、assert関数を使用して条件を確認し、それが満たされない場合にプログラムの実行を停止させます。

exitassert関数が呼び出された後にプログラムの実行を回復する方法はありません。一方通行のエラー処理は、ビジネスフローに関連しない一部の致命的なエラーに対し、プログラムによって処理されることができない場合に実行を止めるために使われます。

このようなエラーの最も単純な例は、コマンドラインインターフェースでパラメータの値がサポートされていない場合です。この場合には、プログラムの実行を止め、エラーメッセージをユーザーに表示することができます。

実行を停止するフローダイアグラム

上のアプローチを使ってエラーを処理するプログラムの流れを、上のダイアグラムで見てみましょう。このダイアグラムには、プログラムのエントリーポイント1から実行するオペレーティングシステムを表すOSレベルが追加されています。

簡単なダイアグラムの説明:

  • osは、通常はmain関数の呼び出しによってプログラムのエントリーポイントを呼び出します。
  • main関数がfunc1関数を呼び出す。
  • func1関数がfunc2関数を呼び出す。
  • func2関数は条件を確認し、プログラムの実行を停止する。
  • osはプログラムからエラーコードを受け取り、それを処理し、プログラムの実行を停止する。

このアプローチは、ビジネスフローに関連しない致命的なエラーが発生した場合にプログラムの実行を止めるために使われます。

長所