エラー処理

レッスン10:エラー処理

現実世界のアナロジー

レストランで料理を注文する場面を想像してください。ウェイターがこう言います:「申し訳ありません、本日この料理は売り切れです。」 — これは予想される状況です。別の料理を注文すればいいだけですし、生活は続きます。これがGoの error です。

しかし、厨房が突然火災を起こし、全員が直ちに避難しなければならない — これは予期しない災害です。これがGoの panic です。

Goのエラー処理哲学は明確です:予想される問題は优雅に処理し、予期しない問題のときだけクラッシュします。 他の言語がすべての例外を try/catch で混ぜるのとは異なり、Goでは各エラーを明示的かつ個別に処理する必要があります。


コアコンセプト

コンセプト 説明
error インターフェース Goの組み込みエラータイプ。メソッドは Error() string のみ
複数の戻り値 Goの関数は通常、最後の戻り値としてエラーを返す
errors.New 簡単なテキストエラーを作成
fmt.Errorf フォーマットされたエラーを作成(%w でラップ可能)
errors.Is エラーチェーンに特定のエラーが含まれているかチェック
errors.As エラーチェーンから特定のエラータイプを抽出
panic ランタイムパニックを引き起こし、プログラムをクラッシュさせる
recover deferpanic をキャッチし、プログラムのクラッシュを防止
エラーラッピング %w またはカスタム型で基礎エラーをラップし、コンテキストを保持

エラー処理フローチャート

関数がエラーを返す
       │
       ▼
  err != nil ?
  ┌────┴────┐
  │ はい    │ いいえ
  ▼         ▼
エラーを処理  続行
  │
  ├─ 回復可能 → ログ / デフォルト値を返す / リトライ
  ├─ 報告が必要 → ラップして上位に渡す
  └─ 回復不可能 → panic

基本構文と使い方

1. error インターフェース

error はGoの組み込みインターフェース型で、非常に簡潔に定義されています。

GO
// error インターフェースの定義(組み込み、インポート不要)
type error interface {
    Error() string
}

Error() string メソッドを実装するすべての型が error です。

2. エラーの作成

GO
package main

import (
    "errors"
    "fmt"
)

func main() {
    // 方法1: errors.New — 簡単なテキストエラーを作成
    err1 := errors.New("ファイルが見つかりません")

    // 方法2: fmt.Errorf — フォーマットされたエラーを作成
    filename := "config.yaml"
    err2 := fmt.Errorf("ファイル %s の読み込みに失敗しました", filename)

    // 方法3: fmt.Errorf + %w — 基礎エラーをラップ(推奨)
    baseErr := errors.New("権限がありません")
    err3 := fmt.Errorf("ログの書き込みができません: %w", baseErr)

    fmt.Println(err1) // ファイルが見つかりません
    fmt.Println(err2) // ファイル config.yaml の読み込みに失敗しました
    fmt.Println(err3) // ログの書き込みができません: 権限がありません
}

3. エラーのチェック

GO
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("レコードが見つかりません")

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return "Alice", nil
}

func main() {
    name, err := findUser(0)
    if err != nil {
        // errors.Is はエラーチェーンにターゲットエラーが含まれているかチェック
        if errors.Is(err, ErrNotFound) {
            fmt.Println("ユーザーが見つかりません。IDを確認してください")
        } else {
            fmt.Println("不明なエラー:", err)
        }
        return
    }
    fmt.Println("ユーザーを見つけました:", name)
}

4. 特定のエラータイプの抽出

GO
package main

import (
    "errors"
    "fmt"
)

// カスタムエラータイプ
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("バリデーションエラー [%s]: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "年齢は0から150の間でなければなりません",
        }
    }
    return nil
}

func main() {
    err := validateAge(200)
    if err != nil {
        // errors.As はエラーチェーンから特定の型を抽出
        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Printf("フィールド: %s、理由: %s\n", ve.Field, ve.Message)
        } else {
            fmt.Println("その他のエラー:", err)
        }
    }
}

5. panic と recover

GO
package main

import "fmt"

// safeDivide は recover を使って panic をキャッチする
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panicをキャッチしました: %v", r)
        }
    }()

    // ゼロ除算は panic を引き起こす
    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("エラー:", err) // エラー: panicをキャッチしました: runtime error: integer divide by zero
        return
    }
    fmt.Println("結果:", result)
}
💡 ヒント: Goでは、エラーの大多数は error を返して処理すべきです。panic を使用するのは、プログラムが本当に続行不可能な場合のみ(初期化エラー、回復不可能なロジックエラーなど)です。

💡 ヒント: fmt.Errorf でエラーをラップするときは、常に %v ではなく %w を使用してください。%w は元のエラーを保持し、errors.Iserrors.As が正しく動作するようにします。

💡 ヒント: エラーメッセージは小文字で句読点なしの文にすべきです。エラーメッセージはしばしば大きなエラーメッセージに連結されるためです。


サンプルコード

例:基本的なエラー処理(難易度⭐)

簡単な設定ファイルリーダーをシミュレートし、最も基本的なエラーの返却とチェックパターンを実演します。

GO
package main

import (
    "errors"
    "fmt"
    "os"
)

// パッケージレベルのセンチネルエラーを定義
var (
    ErrFileNotFound  = errors.New("設定ファイルが見つかりません")
    ErrFileEmpty     = errors.New("設定ファイルが空です")
    ErrInvalidFormat = errors.New("設定ファイルの形式が無効です")
)

// Config はアプリケーション設定を表す
type Config struct {
    Host string
    Port int
}

// LoadConfig はファイルから設定を読み込む(シミュレーション)
func LoadConfig(path string) (*Config, error) {
    // ファイルが見つからない場合をシミュレート
    if path == "" {
        return nil, ErrFileNotFound
    }

    // ファイル読み込みをシミュレート
    data, err := os.ReadFile(path)
    if err != nil {
        // 基礎エラーをラップし、元の情報を保持
        return nil, fmt.Errorf("設定ファイル %s の読み込み: %w", path, err)
    }

    // ファイルが空かチェック
    if len(data) == 0 {
        return nil, ErrFileEmpty
    }

    // 設定の解析をシミュレート
    return &Config{
        Host: "localhost",
        Port: 8080,
    }, nil
}

func main() {
    // 設定の読み込みを試行
    config, err := LoadConfig("app.conf")
    if err != nil {
        // errors.Is で特定のエラータイプを判定
        switch {
        case errors.Is(err, ErrFileNotFound):
            fmt.Println("エラー: 設定ファイルのパスを指定してください")
        case errors.Is(err, ErrFileEmpty):
            fmt.Println("エラー: 設定ファイルが空です。内容を確認してください")
        case errors.Is(err, ErrInvalidFormat):
            fmt.Println("エラー: 設定ファイルの形式が無効です")
        default:
            fmt.Println("エラー:", err)
        }
        return
    }

    fmt.Printf("設定の読み込み成功: %s:%d\n", config.Host, config.Port)
}
▶ 試してみよう

出力(ファイルが存在しない場合):

エラー: 設定ファイルのパスを指定してください

例:エラーラッピングとチェーンチェック(難易度⭐⭐)

多層の関数呼び出しにおけるエラーのラッピング、伝播、チェーンチェックを実演します。

GO
package main

import (
    "errors"
    "fmt"
)

// ビジネスエラーを定義
var (
    ErrUserNotFound = errors.New("ユーザーが見つかりません")
    ErrPermission   = errors.New("権限が不足しています")
)

// データベース層のエラーを表す
type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("データベースエラー [%s.%s]: %e", e.Table, e.Operation, e.Err)
}

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

// UserRepository はユーザーリポジトリ
type UserRepository struct {
    users map[int]string
}

// FindByID はIDでユーザーを検索する
func (r *UserRepository) FindByID(id int) (string, error) {
    name, exists := r.users[id]
    if !exists {
        // データベース層のエラーとしてラップ
        return "", &DatabaseError{
            Operation: "SELECT",
            Table:     "users",
            Err:       ErrUserNotFound,
        }
    }
    return name, nil
}

// Service はビジネスサービス層
type Service struct {
    repo *UserRepository
}

// GetUser はユーザー情報を取得する
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
    name, err := s.repo.FindByID(id)
    if err != nil {
        // 上位にラップし、ビジネスコンテキストを追加
        return "", fmt.Errorf("ユーザー情報の取得 (id=%d): %w", id, err)
    }

    if requireAdmin && name != "admin" {
        return "", fmt.Errorf("ユーザー %s: %w", name, ErrPermission)
    }

    return name, nil
}

func main() {
    repo := &UserRepository{
        users: map[int]string{
            1: "admin",
            2: "Alice",
        },
    }
    svc := &Service{repo: repo}

    // テストシナリオ
    testCases := []struct {
        name         string
        id           int
        requireAdmin bool
    }{
        {"管理者を検索", 1, false},
        {"一般ユーザーを検索", 2, false},
        {"存在しないユーザーを検索", 99, false},
        {"一般ユーザーが管理機能にアクセス", 2, true},
    }

    for _, tc := range testCases {
        fmt.Printf("--- %s ---\n", tc.name)
        user, err := svc.GetUser(tc.id, tc.requireAdmin)
        if err != nil {
            fmt.Printf("  失敗: %v\n", err)

            // errors.Is はエラーチェーンを辿って根本原因を見つけられる
            if errors.Is(err, ErrUserNotFound) {
                fmt.Println("  対応: ユーザーにIDの確認を促す")
            } else if errors.Is(err, ErrPermission) {
                fmt.Println("  対応: ユーザーに管理者への連絡を促す")
            }

            // errors.As は中間層から特定のエラータイプを抽出できる
            var dbErr *DatabaseError
            if errors.As(err, &dbErr) {
                fmt.Printf("  DB詳細: テーブル=%s、操作=%s\n", dbErr.Table, dbErr.Operation)
            }
        } else {
            fmt.Printf("  成功: ユーザー %s\n", user)
        }
        fmt.Println()
    }
}
▶ 試してみよう

出力:

--- 管理者を検索 ---
  成功: ユーザー admin

--- 一般ユーザーを検索 ---
  成功: ユーザー Alice

--- 存在しないユーザーを検索 ---
  失敗: ユーザー情報の取得 (id=99): データベースエラー [users.SELECT]: ユーザーが見つかりません
  対応: ユーザーにIDの確認を促す
  DB詳細: テーブル=users、操作=SELECT

--- 一般ユーザーが管理機能にアクセス ---
  失敗: ユーザー Alice: 権限が不足しています
  対応: ユーザーに管理者への連絡を促す

例:カスタムエラータイプとrecoverの実践(難易度⭐⭐⭐)

カスタムエラータイプ、エラーコード、パニック回復ミドルウェアを備えた完全なHTTPリクエストハンドラを実装します。

GO
package main

import (
    "errors"
    "fmt"
    "runtime/debug"
)

// ==================== カスタムエラーシステム ====================

// AppError はアプリケーションレベルのエラーで、エラーコードとコンテキストを持つ
type AppError struct {
    Code    int               // ビジネスエラーコード
    Message string            // ユーザーフレンドリーなメッセージ
    Detail  string            // 開発者向けデバッグ情報
    Cause   error             // 基礎原因
    Context map[string]string // 追加コンテキスト
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// Is はカスタムエラー比較ロジックを実装
func (e *AppError) Is(target error) bool {
    t, ok := target.(*AppError)
    if !ok {
        return false
    }
    return e.Code == t.Code
}

// NewAppError は新しいアプリケーションエラーを作成
func NewAppError(code int, message string) *AppError {
    return &AppError{
        Code:    code,
        Message: message,
        Context: make(map[string]string),
    }
}

// WithCause は基礎原因を設定
func (e *AppError) WithCause(err error) *AppError {
    e.Cause = err
    return e
}

// WithDetail はデバッグ詳細を設定
func (e *AppError) WithDetail(detail string) *AppError {
    e.Detail = detail
    return e
}

// WithContext はコンテキスト情報を追加
func (e *AppError) WithContext(key, value string) *AppError {
    e.Context[key] = value
    return e
}

// 事前定義されたエラーコード
var (
    ErrBadRequest   = NewAppError(400, "不正なリクエスト")
    ErrUnauthorized = NewAppError(401, "認証されていません")
    ErrForbidden    = NewAppError(403, "アクセスが禁止されています")
    ErrNotFound     = NewAppError(404, "リソースが見つかりません")
    ErrInternal     = NewAppError(500, "内部サーバーエラー")
)

// ==================== ビジネス層のシミュレーション ====================

// Request はHTTPリクエストをシミュレート
type Request struct {
    UserID  int
    Path    string
    IsAdmin bool
}

// Response はHTTPレスポンスをシミュレート
type Response struct {
    StatusCode int
    Body       string
}

// validateRequest はリクエストパラメータを検証する
func validateRequest(req *Request) error {
    if req.UserID <= 0 {
        return ErrBadRequest.
            WithDetail("user_id は正の整数でなければなりません").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    if req.Path == "" {
        return ErrBadRequest.
            WithDetail("path は空にできません")
    }
    return nil
}

// authenticate は認証
func authenticate(req *Request) error {
    if req.UserID == 0 {
        return ErrUnauthorized.WithDetail("認証資格情報がありません")
    }
    return nil
}

// authorize は認可
func authorize(req *Request) error {
    if !req.IsAdmin {
        return ErrForbidden.
            WithDetail("管理者権限が必要です").
            WithContext("user_id", fmt.Sprintf("%d", req.UserID))
    }
    return nil
}

// findResource はリソースを検索する(panic を引き起こす可能性がある)
func findResource(path string) (string, error) {
    // panic を引き起こすバグをシミュレート
    if path == "/crash" {
        var p *int
        *p = 1 // nilポインタのデリファレンス、panic を引き起こす
    }

    resources := map[string]string{
        "/users":   "ユーザーリスト",
        "/profile": "ユーザープロフィール",
    }

    data, exists := resources[path]
    if !exists {
        return "", ErrNotFound.
            WithDetail(fmt.Sprintf("パス %s のリソースが見つかりません", path))
    }
    return data, nil
}

// handleRequest はリクエストを処理する(ビジネスロジック)
func handleRequest(req *Request) (*Response, error) {
    // 1. パラメータを検証
    if err := validateRequest(req); err != nil {
        return nil, err
    }

    // 2. 認証
    if err := authenticate(req); err != nil {
        return nil, err
    }

    // 3. 認可(管理パスのみ)
    if req.Path == "/admin" {
        if err := authorize(req); err != nil {
            return nil, err
        }
    }

    // 4. リソースを検索
    data, err := findResource(req.Path)
    if err != nil {
        return nil, err
    }

    return &Response{
        StatusCode: 200,
        Body:       data,
    }, nil
}

// ==================== 回復ミドルウェア ====================

// RecoveryMiddleware はpanic回復ミドルウェア
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
    return func(req *Request) (resp *Response, err error) {
        defer func() {
            if r := recover(); r != nil {
                // panicのスタックトレースをログに記録
                stack := string(debug.Stack())
                fmt.Printf("[PANIC] %v\nスタック:\n%s\n", r, stack)

                // クラッシュの代わりに内部エラーを返す
                err = ErrInternal.
                    WithDetail(fmt.Sprintf("panic: %v", r)).
                    WithCause(fmt.Errorf("panic: %v", r))
            }
        }()

        return handler(req)
    }
}

// ==================== エラーのフォーマット ====================

// formatError はエラーをフレンドリーなレスポンスにフォーマットする
func formatError(err error) *Response {
    var appErr *AppError
    if errors.As(err, &appErr) {
        msg := fmt.Sprintf("エラー [%d]: %s", appErr.Code, appErr.Message)
        if appErr.Detail != "" {
            msg += fmt.Sprintf("\n  詳細: %s", appErr.Detail)
        }
        if len(appErr.Context) > 0 {
            msg += "\n  コンテキスト:"
            for k, v := range appErr.Context {
                msg += fmt.Sprintf("\n    %s: %s", k, v)
            }
        }
        return &Response{
            StatusCode: appErr.Code,
            Body:       msg,
        }
    }

    return &Response{
        StatusCode: 500,
        Body:       fmt.Sprintf("不明なエラー: %v", err),
    }
}

// ==================== メイン関数 ====================

func main() {
    // Recoveryミドルウェアでハンドラをラップ
    safeHandler := RecoveryMiddleware(handleRequest)

    // さまざまなシナリオをテスト
    testCases := []struct {
        name string
        req  *Request
    }{
        {
            name: "正常なリクエスト",
            req:  &Request{UserID: 1, Path: "/users", IsAdmin: true},
        },
        {
            name: "無効なパラメータ",
            req:  &Request{UserID: -1, Path: "/users"},
        },
        {
            name: "未認証",
            req:  &Request{UserID: 0, Path: "/users"},
        },
        {
            name: "権限不足",
            req:  &Request{UserID: 2, Path: "/admin", IsAdmin: false},
        },
        {
            name: "リソースが見つからない",
            req:  &Request{UserID: 1, Path: "/unknown"},
        },
        {
            name: "panic の発生(自動回復)",
            req:  &Request{UserID: 1, Path: "/crash"},
        },
    }

    for _, tc := range testCases {
        fmt.Printf("=== %s ===\n", tc.name)

        resp, err := safeHandler(tc.req)
        if err != nil {
            resp = formatError(err)
        }

        fmt.Printf("  ステータス: %d\n", resp.StatusCode)
        fmt.Printf("  レスポンス: %s\n\n", resp.Body)
    }
}
▶ 試してみよう

出力:

=== 正常なリクエスト ===
  ステータス: 200
  レスポンス: ユーザーリスト

=== 無効なパラメータ ===
  ステータス: 400
  レスポンス: エラー [400]: 不正なリクエスト
  詳細: user_id は正の整数でなければなりません
  コンテキスト:
    user_id: -1

=== 未認証 ===
  ステータス: 401
  レスポンス: エラー [401]: 認証されていません
  詳細: 認証資格情報がありません

=== 権限不足 ===
  ステータス: 403
  レスポンス: エラー [403]: アクセスが禁止されています
  詳細: 管理者権限が必要です
  コンテキスト:
    user_id: 2

=== リソースが見つからない ===
  ステータス: 404
  レスポンス: エラー [404]: リソースが見つかりません
  詳細: パス /unknown のリソースが見つかりません

=== panic の発生(自動回復) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
スタック:
...
  ステータス: 500
  レスポンス: エラー [500]: 内部サーバーエラー
  詳細: panic: runtime error: invalid memory address or nil pointer dereference

現実世界の使用場面

場面1:データベーストランザクションのロールバック

複数のステップを含むデータベース操作では、いずれかの失敗時にすでに実行された操作をロールバックする必要があります。

GO
package main

import (
    "errors"
    "fmt"
)

var (
    ErrBalanceInsufficient = errors.New("残高不足です")
    ErrAccountFrozen       = errors.New("アカウントが凍結されています")
)

// TxError はトランザクションエラーで、失敗したステップを記録
type TxError struct {
    Step    string
    Cause   error
    Actions []string // ロールバックが必要な実行済みアクション
}

func (e *TxError) Error() string {
    return fmt.Sprintf("トランザクション失敗 [ステップ: %s]: %v", e.Step, e.Cause)
}

func (e *TxError) Unwrap() error {
    return e.Cause
}

// TransferService は振込サービス
type TransferService struct {
    balances map[string]float64
    frozen   map[string]bool
}

// Transfer は振込トランザクションを実行する
func (s *TransferService) Transfer(from, to string, amount float64) error {
    var executedSteps []string

    // ステップ1: 送金元アカウントの検証
    if s.frozen[from] {
        return &TxError{
            Step:  "送金元アカウントの検証",
            Cause: ErrAccountFrozen,
        }
    }
    executedSteps = append(executedSteps, "送金元アカウントのロック")

    // ステップ2: 残高の確認
    if s.balances[from] < amount {
        return &TxError{
            Step:    "残高の確認",
            Cause:   ErrBalanceInsufficient,
            Actions: executedSteps,
        }
    }
    executedSteps = append(executedSteps, "残高チェック通過")

    // ステップ3: 送金元から引落し
    s.balances[from] -= amount
    executedSteps = append(executedSteps, fmt.Sprintf("%.2fを引落し", amount))

    // ステップ4: 送金先アカウントの検証
    if s.frozen[to] {
        return &TxError{
            Step:    "送金先アカウントの検証",
            Cause:   ErrAccountFrozen,
            Actions: executedSteps, // 引落し済みの金額をロールバックする必要あり
        }
    }

    // ステップ5: 送金先に入金
    s.balances[to] += amount

    fmt.Printf("  振込成功: %s -> %s、金額: %.2f\n", from, to, amount)
    return nil
}

func main() {
    svc := &TransferService{
        balances: map[string]float64{
            "Alice": 1000,
            "Bob":   500,
            "Carol": 0,
        },
        frozen: map[string]bool{
            "Carol": true,
        },
    }

    tests := []struct {
        name string
        from string
        to   string
        amt  float64
    }{
        {"正常な振込", "Alice", "Bob", 200},
        {"残高不足", "Alice", "Bob", 9999},
        {"送金先アカウント凍結", "Alice", "Carol", 100},
    }

    for _, t := range tests {
        fmt.Printf("--- %s ---\n", t.name)
        fmt.Printf("  事前: %s=%.2f、%s=%.2f\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])

        err := svc.Transfer(t.from, t.to, t.amt)
        if err != nil {
            fmt.Printf("  失敗: %v\n", err)

            // ロールバックが必要かチェック
            var txErr *TxError
            if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
                fmt.Printf("  %d件の実行済みアクションをロールバック:\n", len(txErr.Actions))
                for _, action := range txErr.Actions {
                    fmt.Printf("    - 取消: %s\n", action)
                }
                // 実際のプロジェクトではここでロールバックロジックを実行
                // 例: svc.balances[from] += amount
            }
        }

        fmt.Printf("  事後: %s=%.2f、%s=%.2f\n\n",
            t.from, svc.balances[t.from], t.to, svc.balances[t.to])
    }
}

場面2:バッチファイル処理とエラー収集

複数のファイルを処理する際、最初の失敗で停止するのではなく、すべてのエラーを収集してまとめて報告します。

GO
package main

import (
    "errors"
    "fmt"
    "strings"
)

var (
    ErrFileCorrupted = errors.New("ファイルが破損しています")
    ErrPermission    = errors.New("権限が不足しています")
    ErrDiskFull      = errors.New("ディスク容量が不足しています")
)

// FileError は単一ファイルのエラー
type FileError struct {
    Path  string
    Op    string
    Cause error
}

func (e *FileError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}

func (e *FileError) Unwrap() error {
    return e.Cause
}

// BatchResult はバッチ処理結果
type BatchResult struct {
    Success []string
    Failed  []FileError
}

func (r *BatchResult) HasErrors() bool {
    return len(r.Failed) > 0
}

func (r *BatchResult) Summary() string {
    var sb strings.Builder
    sb.WriteString(fmt.Sprintf("処理完了: %d件成功、%d件失敗\n",
        len(r.Success), len(r.Failed)))

    if len(r.Success) > 0 {
        sb.WriteString("成功したファイル:\n")
        for _, f := range r.Success {
            sb.WriteString(fmt.Sprintf("  ✓ %s\n", f))
        }
    }

    if len(r.Failed) > 0 {
        sb.WriteString("失敗したファイル:\n")
        for _, e := range r.Failed {
            sb.WriteString(fmt.Sprintf("  ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
        }
    }

    return sb.String()
}

// processFile は単一ファイルの処理をシミュレート
func processFile(path string) error {
    // さまざまなエラーをシミュレート
    fileErrors := map[string]error{
        "corrupted.txt": ErrFileCorrupted,
        "secret.txt":    ErrPermission,
        "huge.txt":      ErrDiskFull,
    }

    if err, exists := fileErrors[path]; exists {
        return &FileError{
            Path:  path,
            Op:    "process",
            Cause: err,
        }
    }
    return nil
}

// processFiles はファイルをバッチ処理する
func processFiles(paths []string) *BatchResult {
    result := &BatchResult{}

    for _, path := range paths {
        err := processFile(path)
        if err != nil {
            result.Failed = append(result.Failed, FileError{
                Path:  path,
                Op:    "process",
                Cause: errors.Unwrap(err),
            })
        } else {
            result.Success = append(result.Success, path)
        }
    }

    return result
}

func main() {
    files := []string{
        "report.pdf",
        "data.csv",
        "corrupted.txt",
        "image.png",
        "secret.txt",
        "huge.txt",
        "readme.md",
    }

    fmt.Printf("%d件のファイルを処理開始...\n\n", len(files))

    result := processFiles(files)
    fmt.Println(result.Summary())

    // エラータイプに基づいて異なる提案を提供
    for _, fe := range result.Failed {
        switch {
        case errors.Is(fe.Cause, ErrFileCorrupted):
            fmt.Printf("提案: %s はバックアップから復元が必要です\n", fe.Path)
        case errors.Is(fe.Cause, ErrPermission):
            fmt.Printf("提案: %s のファイル権限を確認してください\n", fe.Path)
        case errors.Is(fe.Cause, ErrDiskFull):
            fmt.Printf("提案: ディスク容量を空けて %s を再試行してください\n", fe.Path)
        }
    }
}

❓ よくある質問

質問1:panic と error の戻し、いつ使うべき?

原則: 予想される失敗には error を使用し、「決して起きるべきでない」プログラミングエラーには panic を使用します。

GO
// ✓ 正しい: error を返す — ユーザー入力は予測不可能
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数はゼロにできません")
    }
    return a / b, nil
}

// ✓ 正しい: panic を使用 — これはプログラマーのバグで、nilを渡すべきでない
func MustNotNil(v interface{}) {
    if v == nil {
        panic("値はnilであってはなりません。これは呼び出し側のプログラミングエラーです")
    }
}

実践的なルール:

質問2:errors.Is と errors.As の違いは?

GO
// errors.Is: エラーチェーンに特定のセンチネルエラーが含まれているかチェック
// 次と同等: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound)  // true/false

// errors.As: エラーチェーンから特定のエラータイプを抽出
// 型アサーションに似ているが、エラーチェーン全体を走査
var appErr *AppError
errors.As(err, &appErr)  // true + 代入、または false

// 例え話:
// errors.Is → 「あなたはZhang Sanですか?」(アイデンティティの確認)
// errors.As → 「あなたはプログラマーですか?如果是的话、スキルを教えてください」(情報の抽出)

質問3:なぜエラーメッセージは小文字で句読点なしが推奨されるの?

Go公式およびコミュニティの慣例:

GO
// ✓ 推奨
fmt.Errorf("ファイル %s の読み込みに失敗しました: %w", name, err)
// 出力: ファイル config.yaml の読み込みに失敗しました: 権限がありません

// ✗ 非推奨
fmt.Errorf("ファイル %s の読み込みに失敗しました.: %w", name, err)  // 不要な句読点
fmt.Errorf("ファイル %s の読み込みに失敗しました!: %w", name, err)  // 不要な感嘆符
fmt.Errorf("Read file %s failed: %w", name, err)      // 言語が一貫しない

理由は、エラーはしばしばエラーチェーンに連結され、小文字で句読点なしの方が読みやすいからです:

データベースの初期化: postgresへの接続: dial tcp 127.0.0.1:5432: 接続が拒否されました

質問4:ラップされたエラーはどうやって比較するの?

ラップされたエラーを直接 == で比較しても動作しません。errors.Is を使用する必要があります:

GO
var ErrSentinel = errors.New("sentinel")

wrapped := fmt.Errorf("コンテキスト: %w", ErrSentinel)

// ✗ 誤り: 直接比較は失敗
fmt.Println(wrapped == ErrSentinel)  // false

// ✓ 正しい: errors.Is を使用
fmt.Println(errors.Is(wrapped, ErrSentinel))  // true

// errors.Is はエラーチェーン全体を走査:
// wrapped → Unwrap() → ErrSentinel → 一致!

📖 まとめ

トピック 重要ポイント
error インターフェース Error() string メソッドを実装するだけ
エラーの作成 errors.New で簡単なエラー、fmt.Errorf + %w でラップエラー
エラーのチェック errors.Is で型チェック、errors.As で型抽出
カスタムエラー Error() + Unwrap() メソッドを実装し、ビジネスコンテキストを含める
panic/recover 回復不可能なエラーのみ。deferrecover してキャッチ
エラー哲学 明示的な処理、各エラーをチェック、エラーは値であり例外ではない
エラーラッピング %w で基礎エラーを保持、%v で破棄
コミュニティ慣例 エラーメッセージは小文字で句読点なし、最後の戻り値として返す

Goエラー処理の3つの黄金ルール:

  1. すぐにチェックerror を受け取ったら、すぐに err != nil をチェック
  2. 各層でラップ — 各層でコンテキストを追加し、%w で元のエラーを保持
  3. 优雅に降格 — 可能なら回復、できなければ上位に報告、最後の手段としてのみpanic

📝 演習

演習1:リトライ付きファイルリーダーの実装

以下の要件で ReadWithRetry(path string, maxRetries int) ([]byte, error) 関数を書いてください:

GO
// 参考フレームワーク
var ErrMaxRetriesExceeded = errors.New("最大リトライ回数を超えました")

func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        data, err := os.ReadFile(path)
        if err == nil {
            return data, nil
        }
        lastErr = err
        fmt.Printf("  リトライ%d回目失敗: %v\n", i+1, err)
    }
    return nil, fmt.Errorf("%s の読み込み: %w (最後のエラー: %v)", path, ErrMaxRetriesExceeded, lastErr)
}

演習2:エラーレベルシステムの構築

以下のエラーレベルをサポートするエラーレベルシステムを実装してください:

GO
type Level int

const (
    LevelDebug Level = iota
    LevelInfo
    LevelWarn
    LevelError
    LevelFatal
)

type LeveledError struct {
    Level   Level
    Message string
    Cause   error
}

要件:

演習3:エラーコレクターの実装(並行安全)

並行シナリオで複数のgoroutineからエラーを収集する ErrorCollector 型を書いてください:

GO
type ErrorCollector struct {
    // 必要なフィールド
}

// Add はエラーを追加する(並行安全)
func (c *ErrorCollector) Add(err error) { ... }

// Errors は収集したすべてのエラーを返す
func (c *ErrorCollector) Errors() []error { ... }

// HasErrors はエラーがあるかチェック
func (c *ErrorCollector) HasErrors() bool { ... }

// Error はすべてのエラーを1つのエラーにまとめる
func (c *ErrorCollector) Error() error { ... }

要件:


次のレッスン

このレッスンを完了したら、レッスン11:パッケージとモジュールに進んで、Goのコードの整理、依存関係の管理、独自パッケージの公開方法を学びましょう。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%