コンテンツにスキップ

Go/error

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

errorはGoにおいて事前宣言された識別子で、エラー処理のための標準インターフェース型です。エラーを表現するためのGoの基本的な手段であり、単一のメソッドError() stringを持つインターフェースとして定義されています。

基本的な特徴

[編集]
  • インターフェース型: type error interface { Error() string }
  • Error()メソッドを実装した任意の型はerror型として扱える
  • ゼロ値: nil
  • エラー処理の標準的な手段
  • 関数の多値戻り値の最後の要素として慣例的に使用される

他のキーワードとの組み合わせ

[編集]

戻り値としての使用

[編集]
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("ゼロ除算")
    }
    return a / b, nil
}

エラー生成関数

[編集]
// 基本的なエラー生成
err1 := errors.New("シンプルなエラーメッセージ")

// フォーマット付きエラー
err2 := fmt.Errorf("値 %d が範囲外です", value)

// 既存のエラーをラップ(Go 1.13以降)
originalErr := errors.New("元のエラー")
wrappedErr := fmt.Errorf("コンテキスト情報: %w", originalErr)

エラー判定

[編集]
// nilチェック
if err != nil {
    // エラー処理
}

// エラー型の比較(errors.Isを使用 - Go 1.13以降)
if errors.Is(err, io.EOF) {
    // EOFエラーの処理
}

// エラー型のアサーション(errors.Asを使用 - Go 1.13以降)
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("パスエラー:", pathErr.Path)
}

// カスタムエラー型の比較(Go 1.13未満)
if err, ok := err.(*os.PathError); ok {
    fmt.Println("パスエラー:", err.Path)
}

エラーラッピング(Go 1.13以降)

[編集]
// エラーをラップ
err := doSomething()
if err != nil {
    return fmt.Errorf("操作に失敗しました: %w", err)
}

// ラップされたエラーの展開
originalErr := errors.Unwrap(wrappedErr)

カスタムエラー型

[編集]
// 構造体ベースのカスタムエラー
type ValidationError struct {
    Field string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("フィールド %s の検証エラー: %s", e.Field, e.Message)
}

// カスタムエラーの使用
func validateUser(user User) error {
    if user.Name == "" {
        return &ValidationError{Field: "Name", Message: "空にできません"}
    }
    return nil
}

エラー定数

[編集]
var (
    ErrNotFound = errors.New("見つかりません")
    ErrPermission = errors.New("権限がありません")
    ErrTimeout = errors.New("タイムアウト")
)

func findItem(id string) (Item, error) {
    // ...
    return Item{}, ErrNotFound
}

ジェネリクスとの組み合わせ(Go 1.18以降)

[編集]
// エラーを返す可能性のある型を扱うジェネリック関数
func Process[T any](data T) (T, error) {
    var zero T
    // 処理...
    if failed {
        return zero, errors.New("処理に失敗しました")
    }
    return result, nil
}

パニックとエラーの関係

[編集]
// パニックをエラーに変換
func doSomethingRisky() (result string, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("パニックが発生しました: %v", r)
        }
    }()
    
    // パニックが発生する可能性のあるコード
    return "結果", nil
}

// エラーをパニックに変換(通常は避けるべき)
func mustSucceed(err error) {
    if err != nil {
        panic(err)
    }
}

ユースケース

[編集]
  1. 基本的なエラー処理
    • 関数からのエラー返却と検査
       file, err := os.Open("file.txt")
       if err != nil {
           log.Fatalf("ファイルを開けませんでした: %v", err)
       }
       defer file.Close()
    
  2. エラーチェーン(エラーラッピング)
    • エラーにコンテキスト情報を追加しながら伝播
       func processFile(path string) error {
           data, err := readFile(path)
           if err != nil {
               return fmt.Errorf("ファイル読み込み中のエラー: %w", err)
           }
           
           err = processData(data)
           if err != nil {
               return fmt.Errorf("データ処理中のエラー: %w", err)
           }
           
           return nil
       }
    
  3. カスタムエラー型
    • 詳細なエラー情報の提供
       type QueryError struct {
           Query string
           Err   error
       }
       
       func (e *QueryError) Error() string {
           return fmt.Sprintf("クエリ %q の実行中のエラー: %v", e.Query, e.Err)
       }
       
       func (e *QueryError) Unwrap() error { return e.Err }
    
  4. エラー型による分岐処理
    • エラーの種類に基づく異なる処理
       _, err := user.Find(id)
       if errors.Is(err, ErrUserNotFound) {
           // ユーザーが見つからない場合の処理
       } else if errors.Is(err, ErrDatabaseConnection) {
           // データベース接続エラーの処理
       } else if err != nil {
           // その他のエラー処理
       }
    
  5. エラーの無視(特定の状況で)
    • 特定のエラーを無視する場合
       err := os.Remove("/tmp/tempfile")
       if err != nil && !os.IsNotExist(err) {
           log.Printf("ファイル削除中のエラー: %v", err)
       }
    
  6. 複数のエラーの集約
    • 複数の操作から発生したエラーの集約
       type ErrorList []error
       
       func (el ErrorList) Error() string {
           var sb strings.Builder
           sb.WriteString("複数のエラーが発生しました:\n")
           for i, err := range el {
               sb.WriteString(fmt.Sprintf("  %d: %v\n", i+1, err))
           }
           return sb.String()
       }
    
  7. センチネルエラー(事前定義されたエラー値)
    • 特定の状況を表す標準エラー値
       // 標準ライブラリのセンチネルエラーの例
       // io.EOF, sql.ErrNoRows など
       
       // カスタムセンチネルエラー
       var (
           ErrInvalidInput = errors.New("無効な入力")
           ErrNotAuthorized = errors.New("権限がありません")
       )
    
  8. HTTPエラーハンドリング
    • Webアプリケーションでのエラー処理
       func handleUser(w http.ResponseWriter, r *http.Request) {
           user, err := getUser(r.URL.Query().Get("id"))
           if err != nil {
               if errors.Is(err, ErrUserNotFound) {
                   http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
               } else {
                   http.Error(w, "サーバーエラー", http.StatusInternalServerError)
                   log.Printf("ユーザー取得エラー: %v", err)
               }
               return
           }
           // 正常処理...
       }
    
  9. リトライロジック
    • エラーに基づいたリトライ
       func doWithRetry(fn func() error, maxRetries int) error {
           var lastErr error
           for i := 0; i < maxRetries; i++ {
               lastErr = fn()
               if lastErr == nil {
                   return nil
               }
               if !isRetryableError(lastErr) {
                   return fmt.Errorf("致命的エラー (リトライなし): %w", lastErr)
               }
               time.Sleep(backoff(i))
           }
           return fmt.Errorf("最大リトライ回数に達しました: %w", lastErr)
       }
    
  10. エラー監視とロギング
    • エラーの体系的なロギング
        func logError(err error, operation string) {
            if err == nil {
                return
            }
            
            log.Printf("操作 %q 中のエラー: %v", operation, err)
            
            // スタックトレースなどの詳細情報を追加(pkg/errors などを使用)
            // log.Printf("%+v", err)
        }
    
  11. データベース操作エラー処理
    • データベースエラーの適切な処理
        rows, err := db.Query("SELECT * FROM users WHERE id = ?", id)
        if err != nil {
            if errors.Is(err, sql.ErrNoRows) {
                return nil, ErrUserNotFound
            }
            return nil, fmt.Errorf("データベースクエリエラー: %w", err)
        }
        defer rows.Close()
    
  12. グレースフルデグラデーション
    • エラー発生時にサービスの一部を無効化
        recomms, err := getRecommendations(userID)
        if err != nil {
            log.Printf("レコメンデーション取得エラー: %v", err)
            // レコメンデーションなしで続行
            recomms = []Recommendation{}
        }
    

注意点と高度なエラーハンドリング

[編集]
  • エラーメッセージの一貫性: エラーメッセージは一貫した形式で、大文字で始まらず、句読点で終わらないのが慣例です
  • エラーのラッピングとアンラッピング: Go 1.13以降では%w動詞によるエラーラッピングとerrors.Unwrapが標準でサポートされています
      // ラッピング
      err = fmt.Errorf("コンテキスト: %w", originalErr)
      
      // アンラッピング
      originalErr = errors.Unwrap(err)
    
  • エラーチェック関数の作成: エラーの種類を判定するヘルパー関数を作成すると便利です
      func IsNotFoundError(err error) bool {
          return errors.Is(err, ErrNotFound)
      }
    
  • エラートレーシング: スタックトレース付きエラーを提供するサードパーティライブラリ
      // github.com/pkg/errors を使用した例
      import "github.com/pkg/errors"
      
      func loadConfig() error {
          _, err := os.Open("config.json")
          if err != nil {
              return errors.Wrap(err, "設定ファイルを開けません")
          }
          // ...
      }
    
  • エラーハンドリングの集中化: エラー処理ロジックの重複を避けるための集中化
      func handleAPIError(w http.ResponseWriter, err error) {
          switch {
          case errors.Is(err, ErrNotFound):
              responseWithJSON(w, http.StatusNotFound, ErrorResponse{Message: "リソースが見つかりません"})
          case errors.Is(err, ErrUnauthorized):
              responseWithJSON(w, http.StatusUnauthorized, ErrorResponse{Message: "認証が必要です"})
          default:
              responseWithJSON(w, http.StatusInternalServerError, ErrorResponse{Message: "内部サーバーエラー"})
              log.Printf("予期しないエラー: %v", err)
          }
      }
    
  • パッケージレベルでのエラー設計: パッケージのエラーを適切に設計すると、利用者にとっての使いやすさが向上します
      // database/sql パッケージの例
      if err == sql.ErrNoRows {
          // 行が見つからない場合の処理
      }
      
      // カスタムパッケージの例
      package payment
      
      var (
          ErrInsufficientFunds = errors.New("口座残高不足")
          ErrCardExpired = errors.New("カードの有効期限切れ")
      )
    
  • エラーラッピングの深さ: 過度に深いエラーチェーンは避け、適切な抽象化レベルでエラーを整理することが重要です
  • エラー型の適切な設計: 特に公開APIでは、どのようなエラーを返すかを文書化し、一貫した実装を提供することが重要です
  • ゼロ値の安全性: エラー型のゼロ値(nil)との比較は、必ずif err != nilのパターンで行いましょう

Goのエラー処理設計は「明示的なエラーハンドリング」の哲学に基づいており、try-catchのような例外処理メカニズムとは異なるアプローチを取っています。エラー値を明示的に返し、呼び出し側で検査するこの方法により、エラー処理が明確になり、コードの流れが追いやすくなるというメリットがあります。