Go エラーハンドリングのベストプラクティス
Go言語がマルチリターン(多重戻り値)を用いた明示的なエラーチェックを要求するスタイルは、初心者にはやや冗長に映るかもしれません。しかし、この設計こそが開発者にエラーと直面することを強制し、例外(Exceptions)に依存する言語のようにエラーを隠蔽したり、未知のコールスタックへ丸投げしたりすることを防いでいるのです。
本章では、これまでに学んできたすべてのエラーハンドリングメカニズムを総括し、さらなる高みへと昇華させます。実際のエンジニアリングにおいて、これらの技術(if err != nil、カスタムエラー、エラーラッピング、センチネルエラー、そして panic/recover)をどのように運用すれば、コードの明瞭性、メンテナンス性、およびシステム全体のレジリエンス(弾力性)を向上させることができるのかを考察します。
1. コアとなる土台:error 型と if err != nil
改めて強調しますが、Go言語における error は Error() string メソッドのみを持つ組み込みインターフェースです。失敗する可能性のあるすべての関数は、error を最後の戻り値として返すべきです。
1.1 エラーを無視してはならない
if err != nil パターンは Go 言語のエラーハンドリングの魂です。エラーが発生した源泉で即座にインターセプト(遮断)し、処理することを要求します。
package main
import (
"fmt"
"os"
)
func createFile(filename string) (*os.File, error) {
file, err := os.Create(filename)
if err != nil {
// ベストプラクティス:%w を使用して下位エラーをラッピングし、コンテキストを付加する
return nil, fmt.Errorf("ファイル作成に失敗しました [%s]: %w", filename, err)
}
return file, nil
}
func main() {
file, err := createFile("/root/secret.txt") // 権限のないディレクトリでの作成を試行
if err != nil {
// エラー処理は必須!発生しなかったかのように振る舞ってはいけない
fmt.Println("致命的なエラーが発生しました:", err)
return
}
// ベストプラクティス:リソース取得成功後、即座に defer で解放を確実にする
defer file.Close()
fmt.Println("ファイルの作成に成功しました。")
}2. コンテキストの拡充:カスタムエラー型
errors.New が提供する純粋なテキスト情報だけでは、複雑なビジネスロジック(リトライメカニズムや詳細なエラーレポートなど)を支えきれない場合、カスタムエラー型を定義する必要があります。
package main
import (
"fmt"
"time"
)
// 豊富なビジネスコンテキストを保持するカスタムエラーを定義
type InsufficientFundsError struct {
AccountID string
AttemptedWithdrawal float64
Balance float64
Timestamp time.Time
}
// error インターフェースの実装
func (e *InsufficientFundsError) Error() string {
return fmt.Sprintf("アカウント %s の残高が不足しています。引き出し試行額: %.2f, 現在の残高: %.2f, 発生時刻: %s",
e.AccountID, e.AttemptedWithdrawal, e.Balance, e.Timestamp.Format(time.RFC3339))
}
// ビジネス関数
func withdraw(accountID string, amount, balance float64) (float64, error) {
if amount > balance {
return balance, &InsufficientFundsError{
AccountID: accountID,
AttemptedWithdrawal: amount,
Balance: balance,
Timestamp: time.Now(),
}
}
return balance - amount, nil
}
func main() {
_, err := withdraw("USER-888", 1000, 500)
if err != nil {
// 🌟 ベストプラクティス:型アサーション(または errors.As)でカスタムエラーの詳細を抽出する
if fundsErr, ok := err.(*InsufficientFundsError); ok {
fmt.Println("--- 取引拒否の詳細 ---")
fmt.Printf("アカウント: %s\n不足額: %.2f\n", fundsErr.AccountID, fundsErr.AttemptedWithdrawal-fundsErr.Balance)
} else {
fmt.Println("未知のエラー:", err)
}
}
}3. エラーラッピングと追跡:fmt.Errorf と errors パッケージ
モダンな Go 開発(バージョン 1.13 以降)において、エラーラッピング (Error Wrapping) は極めて重要なプラクティスです。エラーが関数呼び出しスタックをさかのぼる際、各レイヤーが独自のコンテキストを付加しつつ、最下層のオリジナルなエラー型を破壊せずに保持できます。
- エラーのラッピング:
fmt.Errorf("... %w", err)を使用します。 - エラーチェーンの判定:
errors.Is(err, targetErr)を使い、エラーチェーン全体に特定のエラーが含まれているか確認します。 - エラーチェーンの抽出:
errors.As(err, &targetPtr)を使い、エラーチェーンを貫通して特定のカスタムエラー型を抽出します。
4. グローバルな指標:センチネルエラー
センチネルエラーとは、パッケージレベルであらかじめ定義された、グローバルに一意なエラー変数のことです。これらは通常、特定の、あるいは予測可能な終端状態を表します。最も有名な例は標準ライブラリの io.EOF(ファイルの終端)です。
package main
import (
"errors"
"fmt"
)
// ベストプラクティス:センチネルエラーは通常 Err で始める
var ErrNotFound = errors.New("リソースが見つかりません")
func fetchResource(id string) (string, error) {
if id != "123" {
return "", ErrNotFound // センチネルエラーを返す
}
return "貴重なデータ", nil
}
func main() {
data, err := fetchResource("456")
if err != nil {
// ベストプラクティス:センチネルエラーの判定には常に errors.Is を使用し、直接比較 (==) は避ける
if errors.Is(err, ErrNotFound) {
fmt.Println("処理ロジック:ユーザーに 404 ページを表示します。")
} else {
fmt.Println("処理ロジック:サーバー内部エラーログを記録します。")
}
return
}
fmt.Println("データ:", data)
}5. 最後の手段:panic と recover の慎重な使用
panic は、プログラムが「不可能かつ回復不能な」災厄状態に陥ったことを示すために使用されます。
ベストプラクティス・ガイドライン:
- ビジネスロジックで Panic を使わない: ネットワークタイムアウト、ユーザーの入力ミス、データベースのレコード不在などはすべて予測されるエラーであり、必ず
errorを返すべきです。 - 初期化失敗時は Panic を許容する: プログラム起動時に必須の設定ファイルが見つからない、あるいは基幹データベースに接続できない場合、即座に
panic(Fail-Fast) させるのが最善の選択です。 - パッケージ境界を越えて Panic させない: 他者が利用するパッケージ(ライブラリ)を開発する場合、外部へ
panicを投げ出すべきではありません。内部で panic が発生した場合は、パッケージの境界でrecoverし、通常のerrorに変換して呼び出し元へ返すべきです。 - Web フレームワークの最終防衛線: 常駐型サービス(HTTP サーバーなど)では、個別のリクエストによるバグでサーバープロセス全体がクラッシュするのを防ぐため、最上位のミドルウェアで
defer recover()を使用する必要があります。
6. Go エラーハンドリング 6 大黄金律 (まとめ)
尊敬される Go コードを書くために、以下の指針を心に刻んでください。
- すべてのエラーをチェックする: 偶然に期待せず、_ を使って戻り値の
errorを無視してはいけません。 - 早期リターン (Early Return): エラーに遭遇したら直ちに
return errし、コードをフラットに保ち、ネスト地獄を回避します。 - コンテキストを付加する: 上位レイヤーへエラーを渡す際は、
fmt.Errorf("%w")でラッピングし、現在の関数が「何をしようとして失敗したのか」を伝えます。 - 一度処理したエラーは返さない: 現在の関数でエラーをログに記録したり、代替処理(フォールバック)を行ったのであれば、そのエラーを再び上位へ
returnしてはいけません。一つのエラーの処理は一度きりです。 - エラーのカテゴリーを区別する: 単純な状態判定にはセンチネルエラーを、動的なデータが必要な複雑なエラーにはカスタム構造体を使用します。
- Panic の使用を抑制する: プログラムが物理的に動作を継続できない極限状態においてのみ、この「核のボタン」を押してください。