Goにおけるエラーチェーン実践入門

こんにちは！フリーランスエンジニアのこたろうです。

エラーチェーンについて、学びで得た知見を共有します。

## エラーチェーンとは

エラーチェーンは、マトリョーシカ（入れ子人形）のように、エラー情報を階層的に保持する仕組みです：

```go
// データベースのエラーを表す構造体
type DBError struct {
    Err error    // 内部エラー（より深い層のエラー）
    Query string // 問題のあったSQL文
}

// バリデーションのエラーを表す構造体
type ValidationError struct {
    Err error    // 内部エラー（DBErrorなども含められる）
    Field string // エラーが発生したフィールド名
}

// 実際の使用例
func createUser(user *User) error {
    // 1. バリデーションチェック
    if err := validateEmail(user.Email); err != nil {
        // EmailのバリデーションエラーをDBエラーでラップ
        dbErr := &DBError{
            Err: err,
            Query: "SELECT * FROM users WHERE email = ?",
        }
        // さらにバリデーションエラーでラップ
        return &ValidationError{
            Err: dbErr,
            Field: "email",
        }
    }
    return nil
}

// エラーが発生した場合の中身
// ValidationError {
//     Field: "email"
//     Err: DBError {
//         Query: "SELECT * FROM users WHERE email = ?"
//         Err: "invalid email format"
//     }
// }
```

このように階層構造にすることで：

1. エラーの発生場所が明確になる（バリデーション→DB→元のエラー）
2. 各層で必要な情報を付加できる（クエリ内容、フィールド名など）
3. デバッグ時に問題を追跡しやすくなる
4. エラーの種類に応じた適切な処理が書きやすくなる

エラーチェーンは、エラーが発生した際の「現場検証の記録」のように、問題が起きた状況を詳細に記録する仕組みといえます。

## errors.Unwrapの活用

エラーチェーンは玉ねぎのような層構造です。`errors.Unwrap`を使うと、各層のエラー情報を1つずつ確認できます：

```go
// エラーが発生する関数の例
func queryUser() error {
    err := db.Query("SELECT * FROM users")
    if err != nil {
        return &DBError{           // 外側：DBエラー
            Err: &ValidationError{ // 中間：検証エラー
                Err: err,          // 中心：元のエラー
                Field: "email",
            },
            Query: "SELECT * FROM users",
        }
    }
    return nil
}

// エラーを層ごとに解析
func inspectError(err error) {
    fmt.Println("エラーを解析します:")
    
    currentError := err    // 現在見ているエラー
    layer := 1            // 何層目かを表す数字
    
    // エラーがnilになるまで繰り返し
    for currentError != nil {
        // 現在の層のエラー内容を表示
        fmt.Printf("層%d: %v\n", layer, currentError)
        
        // 内側の層のエラーを取り出す
        currentError = errors.Unwrap(currentError)
        
        // 次の層へ
        layer++
    }
}

// 実行例
err := queryUser()
inspectError(err)

// 出力例：
// エラーを解析します:
// 層1: DBエラー: SELECT * FROM users の実行失敗
// 層2: バリデーションエラー: emailフィールドが不正
// 層3: 無効なメールアドレス形式です
```

このように`errors.Unwrap`を使うと、エラーの発生箇所から順に、どのような問題が起きたのかを詳しく追跡できます。

## Errorメソッドの実装

`Error()`メソッドは、エラーをテキストとして表示する際の「フォーマット」を定義します。
各エラー型で独自のフォーマットを設定することで、エラー情報をより分かりやすく表示できます：

```go
// DBError用のエラーメッセージフォーマット
func (e *DBError) Error() string {
    return fmt.Sprintf("クエリ '%s' の実行中にエラー発生: %v", e.Query, e.Err)
    // 出力例: "クエリ 'SELECT * FROM users' の実行中にエラー発生: connection refused"
}

// ValidationError用のエラーメッセージフォーマット
func (e *ValidationError) Error() string {
    return fmt.Sprintf("フィールド '%s' の検証エラー: %v", e.Field, e.Err)
    // 出力例: "フィールド 'email' の検証エラー: invalid format"
}

// 実際の使用例
func handleUser(user *User) error {
    err := validateAndSave(user)
    if err != nil {
        log.Printf("エラーが発生: %v", err)  // Error()メソッドが呼ばれる
        
        // 出力例：
        // エラーが発生: フィールド 'email' の検証エラー: クエリ 'SELECT * FROM users' の実行中にエラー発生: connection refused
    }
    return err
}
```

このメソッドが重要な理由：

1. エラーの可読性向上
- 開発者が理解しやすい形式でエラーを表示
- エラーの文脈や重要な情報を含められる

2. ログ出力との相性
- log.Printf や fmt.Printf でエラーを出力する際に自動的に使用される
- デバッグやモニタリングに役立つ情報を含められる

3. エラーチェーンとの連携
- 内部エラー（e.Err）も同様に文字列化される
- エラーチェーン全体を人間が読みやすい形で表示できる

## errors.Isによるエラー判定

特定のエラー型を含むかどうかを判定できます：

```go
var ErrNotFound = errors.New("not found")

func handleError(err error) {
    if errors.Is(err, ErrNotFound) {
        // NotFoundエラーの処理
        return
    }
    // その他のエラー処理
}
```

## errors.Isによる実践的なエラーハンドリング

```go
// 事前に定義しておくエラー
var (
    ErrNotFound = errors.New("resource not found")
    ErrInvalidInput = errors.New("invalid input")
    ErrDatabaseError = errors.New("database error")
)

// DBエラーを表す構造体
type DBError struct {
    Err error
    Query string
}

func (e *DBError) Error() string {
    return fmt.Sprintf("DB error: %s (query: %s)", e.Err, e.Query)
}

// errors.Is()のための実装
func (e *DBError) Is(target error) bool {
    return target == ErrDatabaseError
}

// 実装例
func getUserData(id string) (*User, error) {
    // バリデーション
    if !isValidID(id) {
        return nil, fmt.Errorf("%w: invalid id format", ErrInvalidInput)
    }

    // DB処理
    user, err := db.FindUser(id)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("%w: user not found", ErrNotFound)
        }
        return nil, &DBError{
            Err: err,
            Query: fmt.Sprintf("SELECT * FROM users WHERE id = %s", id),
        }
    }

    return user, nil
}

// エラーハンドリング
func handleUserRequest(w http.ResponseWriter, r *http.Request) {
    user, err := getUserData(r.URL.Query().Get("id"))
    if err != nil {
        switch {
        case errors.Is(err, ErrNotFound):
            http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
        case errors.Is(err, ErrInvalidInput):
            http.Error(w, "入力値が不正です", http.StatusBadRequest)
        case errors.Is(err, ErrDatabaseError):
            http.Error(w, "システムエラーが発生しました", http.StatusInternalServerError)
            log.Printf("DBエラー詳細: %v\n", err)
        default:
            http.Error(w, "予期せぬエラーが発生しました", http.StatusInternalServerError)
        }
        return
    }
    
    json.NewEncoder(w).Encode(user)
}
```

## まとめ

エラーチェーンの利点：
1. エラーの発生経路を正確に追跡可能
2. 詳細なデバッグ情報の保持
3. エラーの種類に応じた適切な処理の実装
4. ログ出力の充実化

これらの機能を組み合わせることで、メンテナンス性の高いエラーハンドリングが実現できます。

参考文献:
-『Goで作るはじめてのWebアプリケーション改訂版』技術書典
- https://techbookfest.org/product/jXDAEU1dR53kbZkgtDm9zx