Go言語のエラーハンドリング|ベストプラクティスと実務パターン完全ガイド

プログラミング

Go言語でコードを書いていて、エラーハンドリングの正しいやり方がよくわからない。書籍やチュートリアルでは基本的なif err != nilしか載っていないけれど、実務ではもっと複雑な状況が多い。エラーをどう構造化すればいいのか、どうロギングすればいいのか、チーム開発ではどう統一すればいいのか――こうした悩みを抱えるエンジニアは少なくありません。

Go言語はシンプルさを重視する言語ですが、エラーハンドリングこそが本当の実力を問う部分です。このガイドでは、Go言語のエラーハンドリングにおけるベストプラクティスから、実務で役立つパターンまで、すぐに使える知識をまとめました。

Go言語のエラーハンドリングが難しい理由

Go言語のエラーハンドリングは、他言語とは異なるアプローチを取っています。JavaやPythonは例外(Exception)の仕組みを持ちますが、Goは明示的なエラー値の返却を採用しています。この設計思想は強力ですが、使い手によって品質が大きく左右される危険性があります。

多くのエンジニアが陥るのは、エラーを単に判定して終わりにしてしまうパターンです。エラーが発生した時の状態、なぜそのエラーが起きたのか、どこまで巻き戻すべきなのかを考えずに書いてしまうと、本番環境でデバッグが極めて困難になります。

Go言語のエラーインターフェースを正しく理解する

Go言語において、エラーは単なるインターフェースです。errorインターフェースはわずか1つのメソッドを持ちます。

type error interface {
    Error() string
}

このシンプルさが、Go言語のエラーハンドリングの核です。どの値でもError()メソッドを実装すれば、エラーとして扱えます。標準ライブラリのerrors.New()fmt.Errorf()を使えば簡単にエラーを作成できます。

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

この基本をおさえた上で、より実務的なパターンに進む必要があります。

エラーラッピングとスタックトレース

Go 1.13以降、fmt.Errorf()%w動詞が追加されました。これはエラーをラッピングする際に元のエラー情報を保持する仕組みです。

func readFile(path string) ([]byte, error) {
    data, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read file %s: %w", path, err)
    }
    return data, nil
}

ラッピングされたエラーは、errors.Is()errors.As()で検査できます。これにより、呼び出し元は元のエラー型を特定し、適切に処理することができます。

data, err := readFile("config.json")
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        // ファイルが見つからない場合の処理
        log.Println("Config file not found")
    } else {
        // その他のエラーの処理
        return err
    }
}

カスタムエラー型の実装

実務では、汎用的なエラーメッセージでは不十分な場合が多くあります。HTTP ステータスコード、エラーコード、内部的な詳細情報など、複数の情報を保持したいなら、カスタムエラー型を定義すべきです。

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error in field %s: %s", e.Field, e.Message)
}

func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "invalid email format",
        }
    }
    return nil
}

カスタムエラー型を使うと、型アサーション(errors.As())で特定のエラー情報にアクセスでき、より細かい制御が可能になります。

err := validateEmail("invalid-email")
if err != nil {
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Error in field: %s\n", valErr.Field)
    }
}

エラーハンドリングの実務パターン集

パターン1:呼び出し元への委譲

最もシンプルで多用されるパターンです。エラーを処理せず、そのまま呼び出し元に返します。このとき、エラーコンテキストを付与することが重要です。

func fetchUserData(userID string) (User, error) {
    resp, err := http.Get("https://api.example.com/users/" + userID)
    if err != nil {
        return User{}, fmt.Errorf("failed to fetch user data: %w", err)
    }
    // 処理続行...
}

パターン2:エラーのログと変換

特定のエラーに対して、内部的なログを記録した上で、呼び出し元には別のエラーを返すパターンです。データベース接続エラーなど、内部情報を隠すべき場合に有効です。

func queryDatabase(query string) ([]Record, error) {
    rows, err := db.Query(query)
    if err != nil {
        log.Errorf("database query failed: %v", err)
        return nil, errors.New("database operation failed")
    }
    // 処理続行...
}

パターン3:リトライロジック

一時的なエラー(ネットワーク障害など)に対しては、リトライが有効です。

func fetchWithRetry(url string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        resp, err := http.Get(url)
        if err == nil {
            defer resp.Body.Close()
            return ioutil.ReadAll(resp.Body)
        }
        lastErr = err
        time.Sleep(time.Duration(math.Pow(2, float64(i))) * time.Second)
    }
    return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}

パターン4:エラー集約

複数の処理が並行して実行される場合、各ゴルーチンのエラーを集約する必要があります。

func processMultiple(items []string) error {
    errChan := make(chan error, len(items))
    for _, item := range items {
        go func(i string) {
            if err := process(i); err != nil {
                errChan <- fmt.Errorf("processing %s failed: %w", i, err)
            } else {
                errChan <- nil
            }
        }(item)
    }

    var errs []error
    for i := 0; i < len(items); i++ {
        if err := <-errChan; err != nil {
            errs = append(errs, err)
        }
    }

    if len(errs) > 0 {
        return fmt.Errorf("multiple errors: %v", errs)
    }
    return nil
}

AIを活用したエラーハンドリング設計

ChatGPTやClaudeなどの生成AIは、エラーハンドリング戦略の設計に役立ちます。特に、既存のコードを分析してエラーパターンを抽出する際に有効です。

例えば、「このGo関数のエラーハンドリングをレビューしてください」とAIに投げれば、見落としているエラーケースや改善点を指摘してくれます。また、Cursor Rulesを活用することで、チーム全体で統一されたエラーハンドリングスタイルを強制できます

Claude codeを使った自動化の極意では、このような設計作業を自動化する技法も紹介されており、大規模なコードベースのリファクタリングを効率化できます。

エラーロギングの戦略

エラーハンドリングと同様に重要なのが、エラーロギングです。単にlog.Println()するだけでは不十分です。構造化ロギング(Structured Logging)を導入することで、本番環境でのトラブルシューティングが格段に効率化されます。

import "go.uber.org/zap"

func processOrder(orderID string) error {
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    order, err := fetchOrder(orderID)
    if err != nil {
        logger.Error("failed to fetch order",
            zap.String("orderID", orderID),
            zap.Error(err),
        )
        return err
    }

    if err := validateOrder(order); err != nil {
        logger.Warn("order validation failed",
            zap.String("orderID", orderID),
            zap.String("reason", err.Error()),
        )
        return err
    }

    return nil
}

構造化ログを使うと、JSON形式で出力でき、ログ集約ツール(DatadogやStackdriverなど)で簡単に検索・分析できます。

エラーハンドリングの比較:Go vs 他言語

項目 Go Python Java
エラーの仕組み 戻り値 例外 例外
実装の明示性 高い(必ずif err != nilを書く) 低い(try-except を忘れやすい) 中程度(チェック例外・非チェック例外がある)
パフォーマンス 優秀(エラーオブジェクト作成のみ) 低い(スタックアンワインド処理) 中程度
スタックトレース自動取得 自動取得なし(手動で実装が必要) 自動取得あり 自動取得あり

Goのアプローチは最初は手間に見えますが、明示的であるがゆえに、エラーハンドリング漏れを防げます。また、パフォーマンスも例外ベースの言語より優秀です。

よくある間違いと対策

間違い1:エラーを無視する

最悪のパターンは、_ = someFunction()としてエラーを無視することです。これは本来はできるだけ避けるべき行為ですが、やむを得ない場合は明示的なコメントをつけましょう。

// エラーは無視可能(この関数は副作用のみが目的)
_ = logger.Sync()

間違い2:エラーを無限ラッピング

エラーハンドリングの層が深くなると、fmt.Errorf()を重ねてしまいがちです。これはエラーメッセージが肥大化し、むしろ可読性を損なわせます。

// 悪い例
return fmt.Errorf("layer1: %w", fmt.Errorf("layer2: %w", fmt.Errorf("layer3: %w", originalErr)))

ラッピングは1階層で十分です。複数の情報が必要なら、カスタムエラー型を使いましょう。

間違い3:型アサーション忘れ

カスタムエラーの詳細情報にアクセスする際、errors.As()を使わず直接キャストしてしまうと、nil参照エラーで異常終了する危険があります。

// 危険な例
err := someFunc()
valErr := err.(*ValidationError)  // nilの場合、パニック
fmt.Println(valErr.Field)

// 安全な例
var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Println(valErr.Field)
}

チーム開発でのエラーハンドリング統一

複数の開発者がいるプロジェクトでは、エラーハンドリングのスタイルが統一されていないと、コードレビューや保守が困難になります。

以下の施策を推奨します。

  • エラーハンドリング標準ガイドをドキュメント化する
  • linter(golangci-lintなど)でエラー無視をチェックする
  • コードレビューテンプレートにエラーハンドリング確認項目を含める
  • 定期的なコード品質監査を実施する

Cursor Rules 書き方・チームで共有する方法で紹介されているように、AI開発ツールを使ってガイドラインを自動化することも有効です。

おすすめ書籍・ガジェット

  • プログラミング言語Go 完全入門 ― Go言語の基礎から実務パターンまで網羅した必読書。エラーハンドリングの章も充実しており、初心者から中級者まで幅広く対応しています。
  • 実践Go言語 本番環境での開発 ― 実務的なエラーハンドリング、ロギング、テストパターンが詳しく解説されています。大規模プロジェクト開発に携わるエンジニア向けです。
  • HHKB Professional Hybrid ― エンジニアの生産性向上に欠かせない高級キーボード。長時間のコーディングでも疲れにくく、複雑なエラーハンドリングロジックの実装に集中できます。

まとめ

Go言語のエラーハンドリングは、シンプルながら奥が深い領域です。基本のif err != nilから始まり、エラーラッピング、カスタム型、ロギングと進むにつれて、本当に堅牢なシステムが構築できるようになります。

実務では、常に「このエラーが発生した時、ユーザーにはどう伝えるのか」「デバッグする際に必要な情報はあるのか」を意識してコードを書くことが重要です。AIツールを活用しながら、チーム全体で標準化されたエラーハンドリング文化を作り上げることで、本番環境でのトラブルを最小限に抑えられます。

30代未経験からAIエンジニアへの転職を目指す方も、このようなベストプラクティスの習得が評価対象になります。エラーハンドリングの質は、プロフェッショナルなエンジニアの証です。

FAQ

Go言語でfmt.Errorf()errors.New()の使い分けは?

errors.New()は静的なメッセージを持つ単純なエラーを作成します。一方、fmt.Errorf()は変数やコンテキスト情報を含む動的なメッセージを作成でき、Go 1.13以降は%w動詞でエラーラッピングも可能です。実務では、ほぼすべての場合でfmt.Errorf()の使用が推奨されます。元のエラー情報を保持したい場合は必ず%wを使い、単なる情報文字列の場合は%v%sを使います。

カスタムエラー型を定義するときの命名規則は?

Go言語の慣例では、カスタムエラー型はErrorで終わる名前をつけます。例えばValidationErrorNotFoundErrorTimeoutErrorなどです。さらに、エクスポート(大文字開始)する必要があります。パッケージ内で限定的に使うなら小文字でもよいですが、他パッケージからerrors.As()で検査される場合はエクスポート必須です。

errors.Is()errors.As()の違いは?

errors.Is()はエラーが特定の値と同じかどうかを判定します。一方、errors.As()はエラーが特定の型かどうかを判定し、型アサーション後に変数に格納します。Is()os.ErrNotExistのような事前定義エラーの判定に使い、As()はカスタムエラー型の詳細情報にアクセスする際に使う、と使い分けます。

複数のエラーを同時に処理する場合、どのようにすればいい?

Go 1.20で導入されたerrors.Join()を使うと複数のエラーを1つにまとめられます。並行処理の場合は、チャネルでエラーを集約し、それらをerrors.Join()でまとめるのが標準的です。あるいは、スライスにエラーを追加し、最終的にlen(errs) > 0で判定する方法もあります。重要なのは、複数エラーの存在を明示的に扱うことです。

Goでエラーハンドリングを自動化できるツールはあるか?

golangci-linterrcheckルールで、エラーの無視をチェックできます。さらに高度な分析には、Claude codeなどのAIツールを使って、既存コードのエラーハンドリングパターンを自動分析・改善することも可能です。ただし、完全自動化は難しく、エンジニアの判断が常に必要です。

タイトルとURLをコピーしました