0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

エラーチェーンとは

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

// データベースのエラーを表す構造体
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つずつ確認できます:

// エラーが発生する関数の例
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()メソッドは、エラーをテキストとして表示する際の「フォーマット」を定義します。
各エラー型で独自のフォーマットを設定することで、エラー情報をより分かりやすく表示できます:

// 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. エラーの可読性向上
  • 開発者が理解しやすい形式でエラーを表示
  • エラーの文脈や重要な情報を含められる
  1. ログ出力との相性
  • log.Printf や fmt.Printf でエラーを出力する際に自動的に使用される
  • デバッグやモニタリングに役立つ情報を含められる
  1. エラーチェーンとの連携
  • 内部エラー(e.Err)も同様に文字列化される
  • エラーチェーン全体を人間が読みやすい形で表示できる

errors.Isによるエラー判定

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

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

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

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

// 事前に定義しておくエラー
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アプリケーション改訂版』技術書典

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?