Go エラーハンドリング 2026年版:パターン、ラッピング、技術面接の頻出質問

Goのエラーハンドリングパターンを網羅的に解説。センチネルエラー、カスタム型、errors.Is・errors.As、%wによるエラーラッピング、技術面接で頻出の質問まで幅広くカバー。

Go error handling patterns and wrapping

Go のエラーハンドリングは、他の主流プログラミング言語とは一線を画すアプローチを採用している。例外処理や try/catch ブロックに依存する言語とは異なり、Go はエラーを値として扱い、関数の戻り値として明示的に返す設計を貫いている。この意図的な設計思想により、エラー処理がコードの前景に押し出され、障害パスの可視性とテスト容易性が大幅に向上する。

例外ではなくエラー値

Go には例外機構が存在しない。失敗する可能性のある関数はすべて、最後の戻り値としてエラーを返す。このパターンにより、エラーを無視するコストがソースコードレベルで可視化され、エラーフローのテストが極めて容易になる。

error インターフェースとその重要性

Go のエラーシステム全体は、標準ライブラリで定義された単一のインターフェースに基づいている:

builtin.gogo
type error interface {
    Error() string
}

Error() string を実装する型はすべてこのインターフェースを満たす。このシンプルさが合成可能性を促進する。エラーは構造体でもラッパーでも、文字列表現を生成できるものであれば何でも構わない。

基本的なカスタムエラー型でこのパターンを確認する:

apperror.gogo
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}

この NotFoundError は構造化されたデータを保持する。呼び出し元は文字列を解析する代わりに、リソース名や ID をプログラム的に抽出できる。これは HTTP API や CLI ツールを構築する際に、エラーを特定のレスポンスにマッピングする場面で決定的な利点となる。

センチネルエラーによる既知の条件表現

センチネルエラーとは、特定の既知の障害条件を表すパッケージレベルの変数である。標準ライブラリではこのパターンが広く使用されている:io.EOFsql.ErrNoRowsos.ErrNotExist などが代表例である。

errors.gogo
var (
    ErrNotFound     = errors.New("record not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrConflict     = errors.New("resource conflict")
)

センチネルエラーは、呼び出し元が詳細な理由ではなく、何が失敗したかだけを知る必要がある場合に最適である。追加のコンテキストを持たずに条件をシグナルする。Go の規約では、Err プレフィックスを付け、Go Code Review Comments ガイドラインに従って、メッセージを小文字で句読点なしに保つ。

センチネルエラーが問題になるケース

センチネルエラーのエクスポートは、パブリック API の契約を生み出す。下流のパッケージは errors.Is でそれらを照合するため、センチネルエラーの改名や削除は破壊的変更となる。呼び出し元が実際に分岐する必要がある条件にのみ使用することが推奨される。

fmt.Errorf と %w による エラーラッピング

Go 1.13 で導入されたエラーラッピングは、元のエラーをチェーン内に保持しながらコンテキストを追加する。fmt.Errorf%w 動詞がこのチェーンを生成する:

repository.gogo
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err)
    }
    return user, nil
}

ラップされたエラーはチェーン全体を保持する。3つ上のレイヤーの呼び出し元でも、元のセンチネルを照合したり、元の型を抽出したりできる。各レイヤーは、何が起こったかを隠すことなく、どこでエラーが発生したかのコンテキストを追加する。

重要な区別として、%w はチェーンを保持してラップするが、%v はエラーを文字列としてフォーマットしてチェーンを切断する。リポジトリがデータベースドライバーのエラーをラップする場合のように、元のエラーが抽象化境界を越えてリークすべきでない場合は、意図的に %v を使用する。

errors.Is と errors.As によるエラー検査

errors パッケージは、直接比較と型アサーションに代わる、ラップされたエラーチェーンを検査する2つの関数を提供する。

errors.Is はチェーンを辿って特定のエラー値を探す:

handler.gogo
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id"))
    if errors.Is(err, ErrNotFound) {
        // ErrNotFound が複数回ラップされていても一致する
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "Internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

errors.As はチェーンから特定のエラー型を抽出する:

middleware.gogo
func errorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func mapErrorToHTTP(err error) int {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        // notFound.Resource と notFound.ID が利用可能
        return http.StatusNotFound
    }
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        return http.StatusBadRequest
    }
    return http.StatusInternalServerError
}

型アサーションに対する主要な利点は、両関数がラップされたチェーン全体を走査することにある。fmt.Errorf%w で3回ラップされた NotFoundError でも問題なく一致する。

レイヤード・アーキテクチャにおける構造化エラーハンドリング

本番環境の Go アプリケーションでは、通常3つのレイヤーにわたってエラーを整理する。ドメインエラーがビジネスレベルの条件を定義し、サービスエラーが運用コンテキストを追加し、ハンドラーやトランスポートエラーが外部レスポンスにマッピングする。

domain/errors.gogo
type DomainError struct {
    Code    string // 機械可読: "USER_NOT_FOUND", "INVALID_INPUT"
    Message string // 人間可読な説明
    Err     error  // 元の原因
}

func (e *DomainError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

func (e *DomainError) Unwrap() error {
    return e.Err
}

Unwrap メソッドが、errors.Iserrors.As をチェーンを通じて動作させる鍵となる。別のエラーをラップするカスタムエラー型は、このメソッドを実装する必要がある。

サービスレイヤーはドメインエラーに運用コンテキストを付与する:

service/user.gogo
func (s *UserService) Deactivate(ctx context.Context, userID string) error {
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        return fmt.Errorf("deactivating user %s: %w", userID, err)
    }
    if user.Status == StatusInactive {
        return &DomainError{
            Code:    "ALREADY_INACTIVE",
            Message: fmt.Sprintf("user %s is already inactive", userID),
        }
    }
    return s.repo.UpdateStatus(ctx, userID, StatusInactive)
}

Goの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Go エラーハンドリングの技術面接頻出質問

Go ポジションの技術面接では、エラーハンドリングに関する知識が頻繁にテストされる。以下の質問はスクリーニングラウンドで定期的に出題され、Go エラーハンドリング面接質問集で詳しくカバーされている。

Go が例外ではなくエラー値を採用している理由は何か?

例外は不可視な制御フローを生み出す。throw する関数は、場合によっては多数のスタックフレーム先にある未知の catch サイトに制御を移す。Go のエラー値は、関数シグネチャ内ですべての障害パスを明示的にする。呼び出し元は即座にエラーの処理方法を決定する。リトライ、ラップ、ログ記録、伝搬のいずれかである。これにより「予期しない throw」問題が排除され、ツールのサポートなしにエラーフローが読み取り可能になる。

fmt.Errorf における %w%v の違いは何か?

%w はエラーをラップし、errors.Iserrors.As のためにチェーンを保持する。%v はエラーを文字列としてフォーマットし、元のエラーへのチェーンリンクを持たない新しいエラーを生成する。呼び出し元が原因を検査できるべき場合は %w を使用し、原因が抽象化境界を越えてリークすべきでない実装の詳細である場合は %v を使用する。

関数がエラーを返すべき場合と panic すべき場合の基準は?

panic は、インデックス範囲外、nil ポインタ参照、不変条件の違反など、バグを示す本当に回復不可能な状況に限定される。ネットワークタイムアウト、レコードの欠如、無効な入力などの回復可能な障害はエラーを返す。実用的なルールとして、通常の運用中に発生し得る条件であればエラーを返し、プログラムにバグがあることを意味する場合は panic する。

Unwrap メソッドはエラーチェーンの検査にどのように影響するか?

カスタムエラー型が Unwrap() error を実装すると、errors.Iserrors.As 関数はそのメソッドに従ってチェーンを走査する。Unwrap がなければ、チェーンはそのエラーで停止する。Go 1.20 以降、エラーは Unwrap() []error を実装して複数のラップされたエラーを返すこともでき、バリデーションエラーの集約のようなケースでツリー形状のエラーチェーンが可能になった。

避けるべきエラーハンドリングのアンチパターン

Go コードベースやコードレビューセッションで頻繁に見られるアンチパターンがいくつか存在する:

エラーの無言での無視。 ブランク識別子は構文上は容易だが、意味論的には危険である:

go
// アンチパターン: エラーの暗黙的な握り潰し
result, _ := riskyOperation()

無視されるすべてのエラーは、意図的かつ文書化された選択であるべきで、決して省略の手抜きであってはならない。

ログ記録と返却の同時実行。 このパターンは重複したログエントリを生成し、同様にエラーを処理する呼び出し元を混乱させる:

go
// アンチパターン: 二重ハンドリング
if err != nil {
    log.Printf("operation failed: %v", err) // ここでログ記録
    return err                               // さらに呼び出し元に返却(再度ログ記録の可能性)
}

修正方法:エラーを処理する(ログ記録、デフォルト値の返却、リトライ)か、伝搬するかのいずれか。両方を行わない。

文字列マッチングによるエラー分類。 err.Error() の出力を strings.Contains で確認するのは脆弱である。エラーメッセージは API 契約ではなく、ライブラリのバージョン間で変更される可能性がある。センチネル値には errors.Is を、型には errors.As を使用する。

同期なしにゴルーチン境界を越えてエラーをラップする。 複数のゴルーチンが同時にエラーを生成する場合、golang.org/x/sync パッケージの errgroup.Group または同様の同期メカニズムを使用する。mutex なしで共有スライスに直接追加すると、データレースが発生する。

errgroup を用いた並行処理のエラーハンドリング

golang.org/x/syncerrgroup パッケージは、並行ゴルーチンからのエラー収集に対するクリーンなパターンを提供する:

batch.gogo
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // 同時ゴルーチン数を制限

    for _, id := range orderIDs {
        g.Go(func() error {
            if err := s.processOrder(ctx, id); err != nil {
                return fmt.Errorf("processing order %s: %w", id, err)
            }
            return nil
        })
    }

    // 最初の非 nil エラーを返し、残りの処理をキャンセル
    return g.Wait()
}

errgroup.WithContext は、いずれかのゴルーチンがエラーを返した時点で派生コンテキストをキャンセルし、他のゴルーチンに停止をシグナルする。これにより無駄な処理が回避され、呼び出し元に単一のラップされたエラーが提供される。

Go 1.24 とエラーハンドリング

Go 1.24(2025年2月)では errors パッケージに変更はなかった。エラーハンドリング API は Go 1.13 以降安定しており、Go 1.20 で Unwrap() []error によるマルチエラーのアンラップが追加されたのが最後の変更である。改善されたエラーハンドリング構文に関する現在の提案は、Go の issue tracker で引き続き議論されている。

まとめ

  • エラーを値として扱い、隠れた例外機構に頼らず、すべての呼び出しサイトで明示的に返却・処理する
  • センチネルエラー(var ErrX = errors.New(...))は呼び出し元が分岐する必要がある条件にのみ使用し、パブリック API として扱う
  • fmt.Errorf%w でコンテキストを追加しながらチェーンを保持する。抽象化境界で意図的にチェーンを切断する場合は %v を使用する
  • == よりも errors.Is を、型アサーションよりも errors.As を優先する。両者はラップされたチェーン全体を走査する
  • カスタムエラー型には Unwrap() error を実装し、チェーン検査が正しく動作するようにする
  • エラーは処理するか伝搬するかのいずれかであり、両方を行わない。ログ記録とリターンの同時実行は重複ノイズを生む
  • 手動のゴルーチン同期の代わりに errgroup を使用して並行エラー収集を行う
  • Go の規約に従い、エラーメッセージは小文字で句読点なしに保つ

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#go
#error-handling
#best-practices
#interview

共有

関連記事