Goでは、Java、C++、JavaScript、Pythonなどの言語と比較して、エラーに対する考え方がやや異なります。Goにおけるエラーは組み込み型errorの値であり、関数は通常、ほかの返却値とともにエラーを呼び出し元へ返します。これにより、エラーの検査と処理は、別個の例外機構へ移されるのではなく、プログラムの実行フローの中で明示的に行われます。
JetBrainsのガイドは、コミュニティ寄稿者のChristoph Bergerが最初に執筆し、その後JetBrains Goブログへ移されたもので、入出力、ネットワーク、データ検証などのエラーを処理するためのさまざまな技法と実践を紹介しています。Go言語の最新の変更に対応するため、2026年8月に更新されました。
返却が出発点
関数に問題を処理するための十分なコンテキストがない場合、その関数はエラーを呼び出し元の関数へ返すべきです。Goでは通常、エラー値を返却リストの最後に置きます。たとえばReadFile()のような関数は、ファイルの内容とエラーを返し、成功時のエラー値はnil、失敗時はそれ以外の値になります。
このガイドは、呼び出し元がエラーを無視せず、直ちに検査すべきだと説明しています。関数がファイルやネットワーク接続などのリソースを開いた場合、終了時にクリーンアップするためdeferを使用すべきですが、まずオープン処理が成功したことを確認してからにします。無効なリソースを閉じようとすると、追加の問題が発生する可能性があるためです。
エラー構造を壊さずにコンテキストを追加する
エラーは、処理または記録されるまでに複数の関数を通過することがあります。その過程で各関数は、失敗した操作や処理していたパスなどの有用な情報を追加できます。しかし、err.Error()を連結してエラーをテキストに変換すると、エラーのチェーンと型構造が失われます。
正しい方法は、%w演算子とともにfmt.Errorf()を使用し、元のエラーを保持したままラッピングすることです。これにより、後からerrors.Unwrap()で1つの層にアクセスしたり、errors.Is()およびerrors.As()でラッピングチェーン内の特定のエラーを検査したりできます。Go 1.26では、As()に対する型安全な代替手段として、ジェネリック関数errors.AsType()が追加されます。この関数は一致したエラーとブール値を返し、従来の方法では実行時に発生する可能性がある型エラーをコンパイラーが検出できるようにします。
複数のエラーとキャンセルされたコンテキスト
エラーが常に直線的なチェーンになるとは限りません。たとえば複数のファイルを処理する場合、一部の処理は成功し、別の処理は失敗することがあります。Go 1.20以降で利用できる標準パッケージのerrors.Join()は、処理に成功した内容を保持しながら、複数のエラーを1つの値にまとめます。
このガイドは、errors.Unwrap()が1つの値を返すため、結合されたエラーを扱う場合はnilを返すと注意を促しています。結合されたエラーへアクセスするには、その値がUnwrap() []errorを提供するインターフェースを実装しているか確認する必要があります。また、Go 1.20以降ではcontext.WithCancelCause()を使用してキャンセルにカスタムの原因を関連付け、その原因をcontext.Cause(ctx)で取得できます。これにより、一般的なcontext.Canceled値だけに頼らずに済みます。
panicが適切なのはいつか?
panicとrecoverは、通常のエラー検査の代替として使うべきではありません。ユーザー入力の不備、ファイルの欠落、ネットワークのタイムアウトなど、予想されるエラーは、通常の返却経路を通じて処理し、返すべきです。
問題が予期せず、意味のある処理方法が存在しない場合は、パニックが適切になります。たとえば、あらかじめ有効性を確認しておくべきだった固定の正規表現のコンパイルに失敗した場合です。HTTPサーバーでは、可能な場合、アプリケーションが遅延関数内でrecover()を使用し、現在のリクエスト処理のクラッシュがほかのリクエストに影響するのを防ぐことがあります。一方、メモリ不足などの場合、アプリケーションには継続するための現実的な選択肢が残されていない可能性があります。
開発者にとって実際に重要なこと
- エラーを無視しない:エラー値を空白識別子に代入したり、唯一の返却値を破棄したりすると、問題の発見が遅れ、その後の影響の診断が難しくなる可能性があります。
- 適切な場所で記録する:関数はエラーを処理するか、呼び出し元へ返すべきです。このガイドは、ライブラリが自動的に記録することを避けるよう推奨しています。ライブラリの利用者は、異なるログや出力先を好む可能性があるためです。
- 適切な型を使用する:カスタムエラー型は追加情報を保持できます。fs.PathErrorは、操作、パス、内部エラーを提供します。
- ネットワークエラーを区別する:net.OpErrorを使用すると接続失敗の性質を検査できます。また、一時的なエラーであれば、再試行や指数バックオフなどの戦略を利用できる場合があります。
- 入出力処理におけるバイト数を活用する:io.Readerインターフェースは、エラーとともに処理されたバイト数を返すため、すべてのデータを再処理するのではなく、処理を再開するのに役立つ可能性があります。
- io.EOFに注意する:この値は、パッケージの仕様上、読み取りストリームが正常に終了したことを示します。必ずしも通常のエラーとして記録すべき失敗ではありません。
- log.Fatal()を無計画に使用しない:これはos.Exit()を呼び出し、遅延関数をスキップします。そのため、このガイドはmain()関数内での使用、またはos.Exit()の使用に限定することを提案しています。
certi.newsによる編集上の結論は、エラーの経路を明確にすることは単なる冗長な記述スタイルではなく、プログラム設計の一部だということです。体系的なラッピング、適切な型、責任ある記録によって診断が実行可能になる一方、panicや一般的なメッセージ、エラーの破棄に依存すると、後で開発者が必要とする情報が隠されてしまいます。実際の判断は、引き続きアプリケーションのコンテキストに左右されます。リクエストサーバーには限定的なリカバリが必要な場合がありますが、回復不能なエラーでは終了して再起動する必要が生じることがあります。