技術Tips

Goのエラーハンドリングパターン完全ガイド

Goにおけるエラーハンドリングの基本から、実践的なパターン、Go 1.13以降の新機能まで詳しく解説します。

佐藤 裕介

佐藤 裕介

CTO / シニアソフトウェアエンジニア

Go エラーハンドリング バックエンド 設計パターン
Goのエラーハンドリングパターン完全ガイド

はじめに

Goのエラーハンドリングは、他の言語のtry-catch文と異なり、明示的にエラーを返して処理します。一見冗長に見えますが、適切なパターンを使えば、堅牢で保守性の高いコードを書くことができます。

この記事では、Goのエラーハンドリングの基本から、実践的なパターン、Go 1.13以降の新機能まで詳しく解説します。

基本: errorインターフェース

Goのerrorは単純なインターフェースです。

type error interface {
    Error() string
}

エラーの生成

import "errors"

// シンプルなエラー
err := errors.New("something went wrong")

// フォーマット付きエラー
err := fmt.Errorf("failed to process user %s", userID)

エラーチェックの基本パターン

func readFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return data, nil
}

カスタムエラー型の定義

構造体によるカスタムエラー

type ValidationError struct {
    Field   string
    Message string
}

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

// 使用例
func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "must contain @ symbol",
        }
    }
    return nil
}

エラーの型アサーション

err := validateEmail("invalid-email")
if err != nil {
    if ve, ok := err.(*ValidationError); ok {
        fmt.Printf("Validation failed on %s: %s\n", ve.Field, ve.Message)
    } else {
        fmt.Printf("Unknown error: %v\n", err)
    }
}

Go 1.13以降のエラーハンドリング

エラーのラップ

fmt.Errorf%w動詞を使ってエラーをラップできます。

func processUser(id string) error {
    user, err := fetchUser(id)
    if err != nil {
        return fmt.Errorf("failed to process user %s: %w", id, err)
    }
    // 処理...
    return nil
}

errors.Is: エラーの比較

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

func getUser(id string) (*User, error) {
    // ...
    if userNotExists {
        return nil, ErrNotFound
    }
    return user, nil
}

// 使用例
user, err := getUser("123")
if errors.Is(err, ErrNotFound) {
    // NotFoundエラーの処理
    return nil, fmt.Errorf("user not found: %w", err)
}

errors.As: エラー型の取得

type TemporaryError interface {
    error
    Temporary() bool
}

func handleError(err error) {
    var tempErr TemporaryError
    if errors.As(err, &tempErr) && tempErr.Temporary() {
        // 一時的なエラーの場合はリトライ
        retry()
    } else {
        // それ以外は諦める
        logError(err)
    }
}

実践パターン1: Sentinel Error

定義済みのエラー値を使って、特定のエラー状態を表現します。

var (
    ErrNotFound      = errors.New("resource not found")
    ErrUnauthorized  = errors.New("unauthorized")
    ErrInvalidInput  = errors.New("invalid input")
)

func getUserByID(id string) (*User, error) {
    if id == "" {
        return nil, ErrInvalidInput
    }

    user, found := db.Find(id)
    if !found {
        return nil, ErrNotFound
    }

    return user, nil
}

// エラーチェック
user, err := getUserByID("123")
if err != nil {
    switch {
    case errors.Is(err, ErrNotFound):
        return http.StatusNotFound
    case errors.Is(err, ErrUnauthorized):
        return http.StatusUnauthorized
    default:
        return http.StatusInternalServerError
    }
}

実践パターン2: カスタムエラー型

より詳細な情報を持つエラー型を定義します。

type DBError struct {
    Op    string // 操作名
    Table string // テーブル名
    Err   error  // 元のエラー
}

func (e *DBError) Error() string {
    return fmt.Sprintf("db error: op=%s table=%s: %v", e.Op, e.Table, e.Err)
}

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

// 使用例
func insertUser(user *User) error {
    err := db.Insert("users", user)
    if err != nil {
        return &DBError{
            Op:    "insert",
            Table: "users",
            Err:   err,
        }
    }
    return nil
}

実践パターン3: 複数のエラーを集約

type MultiError struct {
    Errors []error
}

func (m *MultiError) Error() string {
    var messages []string
    for _, err := range m.Errors {
        messages = append(messages, err.Error())
    }
    return strings.Join(messages, "; ")
}

func (m *MultiError) Add(err error) {
    if err != nil {
        m.Errors = append(m.Errors, err)
    }
}

func (m *MultiError) HasErrors() bool {
    return len(m.Errors) > 0
}

// 使用例
func validateUser(user *User) error {
    var merr MultiError

    if user.Name == "" {
        merr.Add(errors.New("name is required"))
    }
    if user.Email == "" {
        merr.Add(errors.New("email is required"))
    }
    if user.Age < 0 {
        merr.Add(errors.New("age must be positive"))
    }

    if merr.HasErrors() {
        return &merr
    }
    return nil
}

実践パターン4: パニックとリカバリー

通常のエラーハンドリングではなく、パニックを使うべき場面もあります。

// パニックを起こすべき状況
func mustParseConfig(data []byte) *Config {
    config, err := parseConfig(data)
    if err != nil {
        // 設定ファイルの解析エラーは回復不可能
        panic(fmt.Sprintf("config parse error: %v", err))
    }
    return config
}

// リカバリーによるエラーハンドリング
func safeHandler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("panic recovered: %v", err)
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        }
    }()

    // 実際のハンドラ処理
    handleRequest(w, r)
}

実践パターン5: コンテキストとエラー

context.Contextを使ったタイムアウトとキャンセルの処理。

func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %w", err)
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        // タイムアウトやキャンセルを検出
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("request timeout: %w", err)
        }
        if errors.Is(err, context.Canceled) {
            return nil, fmt.Errorf("request canceled: %w", err)
        }
        return nil, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

エラーハンドリングの推奨パターン

1. エラーを適切に処理する

// ❌ 悪い例
data, _ := os.ReadFile("config.json")

// ✅ 良い例
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("failed to read config: %w", err)
}

2. エラーにコンテキストを追加

// ❌ 悪い例
if err != nil {
    return err
}

// ✅ 良い例
if err != nil {
    return fmt.Errorf("failed to process user %s: %w", userID, err)
}

3. エラーの内容を変更しない

// ❌ 悪い例
if err != nil {
    return errors.New(err.Error()) // 元のエラー情報が失われる
}

// ✅ 良い例
if err != nil {
    return fmt.Errorf("processing failed: %w", err)
}

4. センシティブな情報を含めない

// ❌ 悪い例
return fmt.Errorf("authentication failed for password %s", password)

// ✅ 良い例
return errors.New("authentication failed")

まとめ

Goのエラーハンドリングは明示的で冗長に見えますが、以下のメリットがあります:

  • エラー処理のフローが明確
  • エラーが発生する箇所が一目瞭然
  • 型安全なエラーハンドリング

Go 1.13以降のerrors.Iserrors.As、エラーのラップ機能を活用することで、より堅牢なエラーハンドリングが可能になります。

参考リンク

著者について

佐藤 裕介

佐藤 裕介

CTO / シニアソフトウェアエンジニア

サービスに関するお問い合わせ

開発・技術支援に関するご相談はお気軽にお問い合わせください。

お問い合わせ