第11章: エラー処理 - マシンレベル完全解説

学習目標

この章を終えると、以下ができるようになります:

  • error インターフェースの内部構造(tab + data)を完全に理解できる
  • errors.New と fmt.Errorf の実装をアセンブリレベルで説明できる
  • エラーラッピングチェーンのメモリ構造とトラバースを理解できる
  • errors.Is と errors.As の内部アルゴリズムを実装できる
  • panic/recover の動作メカニズムとスタック処理を理解できる
  • パフォーマンスを考慮したエラー処理を実装できる

🔑 Goのエラー処理哲学 - なぜ例外を使わないのか

例外処理のコスト

多くの言語は例外処理(try-catch)を採用していますが、Goは意図的にこれを採用しませんでした。

例外処理の隠れたコスト:

例外スロー時のCPU処理:
1. スタック巻き戻し(Stack Unwinding)
   - 各フレームのデストラクタ実行
   - 例外オブジェクトの構築
   - スタックトレース生成

2. 例外テーブル検索
   - 各関数の例外ハンドラテーブルを走査
   - 適切な catch ブロックを探す
   - 複数レベルをさかのぼる可能性

3. 制御フローの不透明性
   - どの関数で例外が発生するか不明
   - 隠れた制御フローの分岐
   - パフォーマンス予測が困難

Goのアプローチ - 値としてのエラー

// Goのエラー処理
result, err := someOperation()
if err != nil {
    return fmt.Errorf("failed to perform operation: %w", err)
}

メモリとCPUの観点:

スタックレイアウト:
┌────────────────────────┐
│ result: 値            │  ← 通常の戻り値
├────────────────────────┤
│ err: interface{} (16B) │  ← エラーインターフェース
│   tab: *itab          │     8バイト: 型情報
│   data: *errorData    │     8バイト: 実データポインタ
└────────────────────────┘

利点:
- スタック巻き戻し不要
- 例外テーブル不要
- 制御フローが明確
- 予測可能なパフォーマンス

💡 パフォーマンス: エラー値の返却は、通常の戻り値とほぼ同じコスト(16バイトのコピー)です。

error インターフェースの内部構造 - 最もシンプルな設計

基本定義と哲学

type error interface {
    Error() string
}

これは、Goで最も重要なインターフェースの1つで、たった1つのメソッドで構成されています。

インターフェースのメモリレイアウト

var err error = errors.New("something went wrong")

メモリ状態の詳細:

err (iface - 16バイト):
┌──────────────────────────┐  オフセット
│ tab: *itab               │  0 (8バイト)
├──────────────────────────┤
│ data: *errorString       │  8 (8バイト)
└──────────────────────────┘
        │
        ↓
itab 構造体 (32バイト):
┌──────────────────────────┐
│ inter: *interfacetype    │  ← error インターフェース
├──────────────────────────┤
│ _type: *_type            │  ← *errors.errorString 型
├──────────────────────────┤
│ hash: uint32             │  ← 型のハッシュ値
├──────────────────────────┤
│ _: [4]byte               │  ← パディング
├──────────────────────────┤
│ fun: [1]uintptr          │  ← メソッドテーブル(Error())
└──────────────────────────┘
        │
        ↓
ヒープ:
┌──────────────────────────┐
│ errorString struct:      │
│   s: string              │
│     ptr: *byte           │  → "something went wrong"
│     len: 20              │
└──────────────────────────┘

🔑 重要: error は常に16バイトのインターフェース値です。実際のエラーメッセージはヒープに格納されます。

tab ポインタの役割

tab ポインタは、インターフェースの型情報を保持する itab 構造体を指します:

itab の内容:
1. inter: インターフェース型(error)の定義
2. _type: 実際の型(*errors.errorString)の情報
3. hash: 型の高速比較用ハッシュ値
4. fun: メソッドテーブル(Error() メソッドのアドレス)

インターフェースメソッド呼び出し:
err.Error() の実行時:
1. err.tab.fun[0] からメソッドアドレスを取得
2. err.data を第1引数(レシーバ)として渡す
3. メソッドを呼び出し

data ポインタの役割

data ポインタは、実際のエラーデータが格納されているメモリアドレスを指します:

type errorString struct {
    s string
}

メモリレイアウト:

errorString (24バイト):
┌──────────────────────────┐
│ s: string (16バイト)     │
│   ptr: *byte (8バイト)   │  → 文字列データ
│   len: int (8バイト)     │  → 20
└──────────────────────────┘

errors.New の内部実装 - ヒープ割り当ての詳細

標準ライブラリの実装

// 標準ライブラリの実装
package errors

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

func New(text string) error {
    return &errorString{s: text}
}

アセンブリレベルでの処理フロー

; errors.New("test") の呼び出し全体

; === ステップ1: errorString 構造体をヒープに割り当て ===
MOVQ    $24, AX              ; サイズ = 24バイト(string の ptr + len)
LEAQ    type.errors.errorString, BX  ; 型情報
CALL    runtime.newobject    ; ヒープ割り当て
; 戻り値 AX = 新しいメモリアドレス

; === ステップ2: 文字列をコピー ===
; 引数の string は既にスタックにある(ptr, len)
MOVQ    string_ptr(SP), BX   ; 文字列ポインタを取得
MOVQ    string_len(SP), CX   ; 文字列長を取得

; errorString.s にセット
MOVQ    BX, 0(AX)            ; s.ptr をセット
MOVQ    CX, 8(AX)            ; s.len をセット

; === ステップ3: error インターフェースを構築 ===
LEAQ    go.itab.*errors.errorString,error, BX  ; itab アドレス
MOVQ    BX, 16(SP)           ; err.tab にセット(戻り値の位置)
MOVQ    AX, 24(SP)           ; err.data にセット

RET                          ; 戻る

ヒープ割り当てのコスト

runtime.newobject の処理:
1. GC のヒープから24バイトを確保
2. メモリを初期化(ゼロクリア)
3. GC のマーキングビットを設定
4. ポインタを返す

コスト:
- CPU: ~50-100ns(高速パス)
- CPU: ~500ns-1µs(スローパス、GC トリガー)
- メモリ: 24バイト + GC オーバーヘッド

💡 ヒープ割り当て: errors.New は常にヒープ割り当てを行います。頻繁に呼ばれる場合は、センチネルエラーの使用を検討しましょう。

文字列リテラルの最適化

// コンパイル時定数の場合
err := errors.New("file not found")

// コンパイラ最適化:
// 文字列 "file not found" はデータセグメントに配置され、
// 実行時に毎回割り当てる必要はありません

メモリマップ:

データセグメント:
┌──────────────────────────────┐
│ 文字列リテラル:              │
│   "file not found\x00"       │  ← 固定アドレス
└──────────────────────────────┘

ヒープ:
┌──────────────────────────────┐
│ errorString:                 │
│   s.ptr → データセグメント   │  ← 参照のみ
│   s.len = 14                 │
└──────────────────────────────┘

センチネルエラー - 定数としてのエラー

センチネルエラーの定義

package io

var (
    EOF              = errors.New("EOF")
    ErrUnexpectedEOF = errors.New("unexpected EOF")
    ErrShortWrite    = errors.New("short write")
    ErrShortBuffer   = errors.New("short buffer")
)

メモリ配置とライフサイクル

プログラムロード時:
1. データセグメントに error インターフェースを配置
2. init() 関数で errors.New を呼び出し
3. 結果をグローバル変数に保存

データセグメント(プログラム起動時に初期化):
┌──────────────────────────────────────┐  アドレス
│ io.EOF (error interface):           │  0x10000
│   tab: *itab                        │  → 固定の itab
│   data: *errorString                │  → 固定のエラー文字列
├──────────────────────────────────────┤
│ io.ErrUnexpectedEOF:                │  0x10010
│   tab: *itab                        │
│   data: *errorString                │
├──────────────────────────────────────┤
│ io.ErrShortWrite:                   │  0x10020
│   tab: *itab                        │
│   data: *errorString                │
└──────────────────────────────────────┘

利点:
1. 一度だけ割り当て(プログラム起動時)
2. ポインタ比較で高速判定
3. メモリ効率が良い(全プログラムで1つのみ)
4. スレッドセーフ(読み取り専用)

センチネルエラーの比較

func ReadFile(name string) ([]byte, error) {
    file, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    data, err := io.ReadAll(file)
    if err == io.EOF {  // ポインタ比較
        return data, nil
    }
    return data, err
}

アセンブリレベルの比較:

; err == io.EOF の比較

; === err のアドレスをロード ===
MOVQ    err+0(SP), R1        ; err.tab を R1 にロード
MOVQ    err+8(SP), R2        ; err.data を R2 にロード

; === io.EOF のアドレスをロード ===
LEAQ    io.EOF(SB), R3       ; io.EOF のアドレスを R3 に
MOVQ    0(R3), R4            ; io.EOF.tab を R4 にロード
MOVQ    8(R3), R5            ; io.EOF.data を R5 にロード

; === tab を比較 ===
CMPQ    R1, R4
JNE     not_equal            ; tab が異なれば not_equal へ

; === data を比較 ===
CMPQ    R2, R5
JNE     not_equal            ; data が異なれば not_equal へ

; === 等しい ===
MOVQ    $1, AX               ; true
JMP     done

not_equal:
MOVQ    $0, AX               ; false

done:
; AX に比較結果

⚠️ 落とし穴: == による比較は、エラーがラップされていると機能しません。errors.Is を使用しましょう。

// これは動作しない
err := fmt.Errorf("failed: %w", io.EOF)
if err == io.EOF {  // false!
    // 実行されない
}

// これは動作する
if errors.Is(err, io.EOF) {  // true
    // 実行される
}

fmt.Errorf と %w 動詞 - エラーラッピングの実装

fmt.Errorf の基本

func readConfig(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("failed to read config: %w", err)
    }
    return data, nil
}

wrapError の内部構造

fmt.Errorf は、内部的に wrapError 構造体を作成します:

// 標準ライブラリの内部実装(簡略化)
package fmt

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

メモリレイアウト:

wrapError 構造体:
┌──────────────────────────────┐  オフセット  サイズ
│ msg: string                  │  0          16バイト
│   ptr: *byte                 │  0          8バイト
│   len: int                   │  8          8バイト
├──────────────────────────────┤
│ err: error                   │  16         16バイト
│   tab: *itab                 │  16         8バイト
│   data: *errorData           │  24         8バイト
└──────────────────────────────┘
合計: 32バイト

fmt.Errorf の実装詳細

// 標準ライブラリの実装(簡略化)
func Errorf(format string, a ...interface{}) error {
    // フォーマット文字列を解析
    p := newPrinter()
    p.wrapErrs = true
    p.doPrintf(format, a)
    s := string(p.buf)

    // %w 動詞が使われている場合
    if p.wrappedErr != nil {
        return &wrapError{msg: s, err: p.wrappedErr}
    }

    // 通常のエラー
    return errors.New(s)
}

処理フロー:

fmt.Errorf("failed: %w", originalErr) の処理:

1. フォーマット文字列を解析
   - "failed: %w" を検出
   - %w が存在するので wrapError を使用

2. メッセージを構築
   - "failed: " + originalErr.Error()
   - 結果: "failed: original error message"

3. wrapError を作成
   msg = "failed: original error message"
   err = originalErr

4. ヒープに割り当て
   - 32バイトの wrapError 構造体
   - メッセージ文字列(別途割り当て)

5. error インターフェースとして返す
   tab = (*wrapError, error) の itab
   data = wrapError のポインタ

エラーチェーンのメモリ構造

// 3段階のエラーラッピング
func level1() error {
    return errors.New("original error")
}

func level2() error {
    err := level1()
    return fmt.Errorf("level 2: %w", err)
}

func level3() error {
    err := level2()
    return fmt.Errorf("level 3: %w", err)
}

メモリチェーン:

レベル3のエラー:
┌──────────────────────────────────┐
│ error interface:                 │
│   tab: (*wrapError, error)       │
│   data: ─────────┐               │
└──────────────────┼───────────────┘
                   ↓
┌──────────────────────────────────┐
│ wrapError:                       │
│   msg: "level 3: level 2: ..."  │  ← 完全なメッセージ
│   err: ─────────┐                │
└─────────────────┼────────────────┘
                  ↓
レベル2のエラー:
┌──────────────────────────────────┐
│ error interface:                 │
│   tab: (*wrapError, error)       │
│   data: ─────────┐               │
└──────────────────┼───────────────┘
                   ↓
┌──────────────────────────────────┐
│ wrapError:                       │
│   msg: "level 2: original ..."  │
│   err: ─────────┐                │
└─────────────────┼────────────────┘
                  ↓
レベル1のエラー:
┌──────────────────────────────────┐
│ error interface:                 │
│   tab: (*errorString, error)     │
│   data: ─────────┐               │
└──────────────────┼───────────────┘
                   ↓
┌──────────────────────────────────┐
│ errorString:                     │
│   s: "original error"            │
└──────────────────────────────────┘

メモリ使用量:
- 各 error インターフェース: 16バイト × 3 = 48バイト
- 各 wrapError 構造体: 32バイト × 2 = 64バイト
- errorString 構造体: 16バイト
- 文字列データ: 約100バイト(3つのメッセージ)
合計: ~230バイト

💡 エラーチェーン: 各レベルがコンテキストを追加し、元のエラーを保持します。

%w と %v の違い

// %w: エラーラッピング(Unwrap 可能)
err1 := fmt.Errorf("wrapped: %w", originalErr)

// %v: 単純なフォーマット(Unwrap 不可)
err2 := fmt.Errorf("formatted: %v", originalErr)

メモリ構造の違い:

%w の場合:
┌──────────────────────────────────┐
│ wrapError:                       │
│   msg: "wrapped: original"       │
│   err: → originalErr             │  ← Unwrap で取得可能
└──────────────────────────────────┘

%v の場合:
┌──────────────────────────────────┐
│ errorString:                     │
│   s: "formatted: original"       │  ← メッセージのみ
└──────────────────────────────────┘  originalErr への参照なし

errors.Is - エラー同一性チェックの実装

errors.Is の基本使用法

func processFile(name string) error {
    data, err := os.ReadFile(name)
    if err != nil {
        if errors.Is(err, fs.ErrNotExist) {
            return fmt.Errorf("file does not exist: %w", err)
        }
        return fmt.Errorf("failed to read file: %w", err)
    }
    // 処理...
    return nil
}

errors.Is の内部実装

// 標準ライブラリの実装(簡略化)
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    // リフレクションで比較可能かチェック
    isComparable := reflectlite.TypeOf(target).Comparable()

    for {
        // 直接比較
        if isComparable && err == target {
            return true
        }

        // カスタム Is メソッドを持つ場合
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }

        // Unwrap してチェーンをたどる
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

アルゴリズムフローチャート:

errors.Is(err, target) の処理フロー:

┌─────────────────────┐
│ target が nil?     │
└──────┬──────────────┘
       │ YES
       ├──→ err == target を返す
       │
       │ NO
       ↓
┌─────────────────────┐
│ err が nil?        │
└──────┬──────────────┘
       │ YES
       ├──→ false を返す
       │
       │ NO
       ↓
┌─────────────────────┐
│ err == target?     │
└──────┬──────────────┘
       │ YES
       ├──→ true を返す
       │
       │ NO
       ↓
┌─────────────────────┐
│ err に Is メソッド?│
└──────┬──────────────┘
       │ YES
       ├──→ err.Is(target) を呼び出し
       │    └→ true なら返す
       │
       │ NO または false
       ↓
┌─────────────────────┐
│ err = err.Unwrap() │
└──────┬──────────────┘
       │
       └──→ 最初に戻る(ループ)

errors.Is の実行例

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

err := fmt.Errorf("step 3: %w",
        fmt.Errorf("step 2: %w",
          ErrNotFound))

errors.Is(err, ErrNotFound)

実行トレース:

errors.Is(err, ErrNotFound) の実行:

イテレーション 1:
┌────────────────────────────────┐
│ err = wrapError{"step 3: ..."}│
│ target = ErrNotFound          │
└────────────────────────────────┘
  err == target? → NO
  err.Is(target)? → メソッドなし
  Unwrap → step 2 のエラー

イテレーション 2:
┌────────────────────────────────┐
│ err = wrapError{"step 2: ..."}│
│ target = ErrNotFound          │
└────────────────────────────────┘
  err == target? → NO
  err.Is(target)? → メソッドなし
  Unwrap → ErrNotFound

イテレーション 3:
┌────────────────────────────────┐
│ err = ErrNotFound             │
│ target = ErrNotFound          │
└────────────────────────────────┘
  err == target? → YES!
  → true を返す

⚠️ パフォーマンス: エラーチェーンが長い場合、errors.Is は線形時間O(n)かかります。

カスタム Is メソッドの実装

type TemporaryError struct {
    Err error
}

func (e *TemporaryError) Error() string {
    return fmt.Sprintf("temporary error: %v", e.Err)
}

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

// カスタム Is メソッド
func (e *TemporaryError) Is(target error) bool {
    // 任意の TemporaryError と一致
    _, ok := target.(*TemporaryError)
    return ok
}

使用例:

var ErrTemporary = &TemporaryError{}

func doOperation() error {
    return &TemporaryError{Err: errors.New("connection timeout")}
}

func main() {
    err := doOperation()

    // カスタム Is が呼ばれる
    if errors.Is(err, ErrTemporary) {
        fmt.Println("Temporary error, retrying...")
    }
}

errors.As - 型アサーションチェーンの実装

errors.As の基本使用法

func handleError(err error) {
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Printf("Operation: %s\n", pathErr.Op)
        fmt.Printf("Path: %s\n", pathErr.Path)
        fmt.Printf("Error: %v\n", pathErr.Err)
        return
    }

    var httpErr *HTTPError
    if errors.As(err, &httpErr) {
        fmt.Printf("HTTP %d: %s\n", httpErr.StatusCode, httpErr.URL)
        return
    }

    fmt.Printf("Unknown error: %v\n", err)
}

errors.As の内部実装

// 標準ライブラリの実装(簡略化)
func As(err error, target interface{}) bool {
    if target == nil {
        panic("errors: target cannot be nil")
    }

    // target がポインタのポインタであることを確認
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.Elem().Kind() != reflectlite.Ptr {
        panic("errors: target must be a non-nil pointer to either a type that implements error, or to any interface type")
    }

    targetType := typ.Elem()

    for {
        // 型が一致するかチェック
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            val.Elem().Set(reflectlite.ValueOf(err))
            return true
        }

        // カスタム As メソッドを持つ場合
        if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
            return true
        }

        // Unwrap してチェーンをたどる
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

アルゴリズムフロー:

errors.As(err, &target) の処理:

┌─────────────────────────────┐
│ target のバリデーション     │
│ - nil でないこと            │
│ - ポインタのポインタであること│
└──────────┬──────────────────┘
           ↓
┌─────────────────────────────┐
│ err が nil?                 │
└──────────┬──────────────────┘
           │ YES → false を返す
           │
           │ NO
           ↓
┌─────────────────────────────┐
│ err の型が target に        │
│ 代入可能?                   │
└──────────┬──────────────────┘
           │ YES
           ├──→ *target = err
           │    true を返す
           │
           │ NO
           ↓
┌─────────────────────────────┐
│ err に As メソッド?         │
└──────────┬──────────────────┘
           │ YES
           ├──→ err.As(&target)
           │    └→ true なら返す
           │
           │ NO または false
           ↓
┌─────────────────────────────┐
│ err = err.Unwrap()          │
└──────────┬──────────────────┘
           │
           └──→ 最初に戻る(ループ)

メモリ操作の詳細

var pathErr *os.PathError
errors.As(err, &pathErr)

メモリ状態の変化:

処理前:
スタック:
┌──────────────────────┐
│ pathErr: nil         │  ← ポインタ(8バイト)
└──────────────────────┘

errors.As(&pathErr) の呼び出し:
1. &pathErr のアドレス = 0x7ffc... (スタック上)
2. target = interface{} で受け取る
   ┌──────────────────────┐
   │ tab: (*os.PathError) │
   │ data: 0x7ffc...      │  ← pathErr のアドレス
   └──────────────────────┘

処理中(エラーチェーンを走査):
err1: wrapError → Unwrap
err2: wrapError → Unwrap
err3: *os.PathError → 型一致!

処理後:
スタック:
┌──────────────────────┐
│ pathErr: 0x8000...   │  → os.PathError インスタンス
└──────────────────────┘

リフレクションによる代入:
val.Elem().Set(reflectlite.ValueOf(err))
↓
*(*os.PathError)(0x7ffc...) = err  // ポインタを通じて代入

🔑 型アサーションとの違い: errors.As はエラーチェーンをたどるため、ラップされたエラーも検出できます。

// 型アサーション: ラップされたエラーは検出できない
pathErr, ok := err.(*os.PathError)  // false

// errors.As: ラップされたエラーも検出できる
var pathErr *os.PathError
errors.As(err, &pathErr)  // true

カスタム As メソッドの実装

type MultiError struct {
    Errors []error
}

func (e *MultiError) Error() string {
    return fmt.Sprintf("multiple errors: %d", len(e.Errors))
}

// カスタム As メソッド
func (e *MultiError) As(target interface{}) bool {
    // 各エラーに対して As を試行
    for _, err := range e.Errors {
        if errors.As(err, target) {
            return true
        }
    }
    return false
}

使用例:

func processMultiple() error {
    return &MultiError{
        Errors: []error{
            &os.PathError{Op: "open", Path: "file1.txt", Err: fs.ErrNotExist},
            errors.New("another error"),
        },
    }
}

func main() {
    err := processMultiple()

    var pathErr *os.PathError
    // カスタム As が呼ばれ、最初の PathError を見つける
    if errors.As(err, &pathErr) {
        fmt.Printf("Found path error: %s\n", pathErr.Path)
    }
}

panic と recover - Go の例外メカニズム

panic の基本動作

func riskyOperation() {
    panic("something went wrong")
}

func main() {
    riskyOperation()
    fmt.Println("This will not be printed")
}

panic のスタック処理

panic が発生すると、以下の処理が行われます:

panic の実行フロー:

1. panic 値の作成
   ┌──────────────────────────┐
   │ _panic 構造体:           │
   │   arg: interface{}       │  ← panic の引数
   │   link: *_panic          │  ← 前の panic(ネスト用)
   │   recovered: bool        │  ← recover されたか
   │   aborted: bool          │  ← 中断されたか
   │   pc: uintptr            │  ← panic 発生位置
   │   sp: uintptr            │  ← スタックポインタ
   │   goexit: bool           │
   └──────────────────────────┘

2. defer チェーンの実行
   - 現在の関数の defer を逆順に実行
   - 各 defer で recover() をチェック
   - recover() が呼ばれたら、panic を停止

3. スタック巻き戻し
   - 関数を1つ戻る
   - その関数の defer を実行
   - recover() されるまで繰り返す

4. recover されない場合
   - プログラム終了
   - スタックトレースを出力

メモリ構造:

Goroutine のスタック:

┌──────────────────────────┐  ← SP (スタックポインタ)
│ 現在の関数フレーム       │
├──────────────────────────┤
│ defer リスト:            │
│   defer3 → defer2 → defer1
│   ↑                      │
│   実行順序(逆順)       │
├──────────────────────────┤
│ panic チェーン:          │
│   _panic 構造体          │
│   link → 前の panic      │
├──────────────────────────┤
│ 呼び出し元フレーム       │
│   defer リスト           │
├──────────────────────────┤
│ ...                      │
└──────────────────────────┘

defer と panic の相互作用

func example() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")

    panic("oops")

    fmt.Println("This will not execute")
}

実行順序:

example() の実行:

1. defer 3 を登録
2. defer 2 を登録
3. defer 1 を登録
4. panic("oops") 発生
   ↓
5. defer 1 実行 → "defer 1" 出力
6. defer 2 実行 → "defer 2" 出力
7. defer 3 実行 → "defer 3" 出力
8. 呼び出し元に伝播(recover されていないため)

recover の動作メカニズム

func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered from: %v\n", r)
        }
    }()

    panic("something went wrong")
}

recover の処理:

recover() の内部処理:

1. 現在の goroutine の _panic をチェック
   goroutine.panic != nil?

2. panic が存在し、まだ recovered でない場合
   ┌──────────────────────────┐
   │ _panic.recovered = true  │  ← フラグを立てる
   │ return _panic.arg        │  ← panic の値を返す
   └──────────────────────────┘

3. panic が存在しない場合
   return nil

4. panic 処理の停止
   - スタック巻き戻しを停止
   - defer 実行を完了
   - 通常の実行フローに戻る

メモリ状態の変化:

panic 発生時:
┌──────────────────────────────┐
│ goroutine:                   │
│   panic: *_panic             │ → ┌──────────────────┐
│     arg: "error"             │   │ _panic:          │
│     recovered: false         │   │   arg: "error"   │
│     link: nil                │   │   recovered: false│
└──────────────────────────────┘   └──────────────────┘

recover() 呼び出し後:
┌──────────────────────────────┐
│ goroutine:                   │
│   panic: *_panic             │ → ┌──────────────────┐
│     arg: "error"             │   │ _panic:          │
│     recovered: true          │   │   arg: "error"   │  ← フラグ変更
│     link: nil                │   │   recovered: true │
└──────────────────────────────┘   └──────────────────┘

defer 完了後:
┌──────────────────────────────┐
│ goroutine:                   │
│   panic: nil                 │  ← panic 削除
└──────────────────────────────┘

ネストした panic

func nestedPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Outer recovered: %v\n", r)
            panic("new panic")  // 新しい panic
        }
    }()

    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Inner recovered: %v\n", r)
        }
    }()

    panic("original panic")
}

panic チェーンの構造:

nestedPanic() の実行:

最初の panic:
┌──────────────────────────────┐
│ goroutine.panic:             │
│   _panic1:                   │
│     arg: "original panic"    │
│     link: nil                │
│     recovered: false         │
└──────────────────────────────┘

内側の defer で recover:
┌──────────────────────────────┐
│ goroutine.panic:             │
│   _panic1:                   │
│     arg: "original panic"    │
│     recovered: true          │  ← 回復
└──────────────────────────────┘
出力: "Inner recovered: original panic"

外側の defer で新しい panic:
┌──────────────────────────────┐
│ goroutine.panic:             │
│   _panic2:                   │  ← 新しい panic
│     arg: "new panic"         │
│     link: → _panic1          │  ← 前の panic にリンク
│     recovered: false         │
└──────────────────────────────┘

外側の defer で recover:
┌──────────────────────────────┐
│ goroutine.panic:             │
│   _panic2:                   │
│     arg: "new panic"         │
│     recovered: true          │  ← 回復
└──────────────────────────────┘
出力: "Outer recovered: new panic"

panic のパフォーマンスコスト

panic/recover のコスト:

1. panic 発生:
   - _panic 構造体の割り当て: ~100ns
   - スタックトレースの生成: ~10µs
   - defer チェーンの走査: ~100ns/defer

2. スタック巻き戻し:
   - 関数フレームごとの処理: ~500ns
   - defer 実行: 通常の defer コスト

3. recover:
   - フラグ設定: ~10ns
   - 制御フロー復元: ~100ns

合計: ~10-50µs(深さに依存)

⚠️ 注意: panic/recover は通常のエラー処理に使うべきではありません。本当の例外的状況のみに使用してください。

カスタムエラー型 - 構造化されたエラー情報

基本的なカスタムエラー

type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error [%s]: %s (value: %v)",
        e.Field, e.Message, e.Value)
}

メモリレイアウト:

ValidationError 構造体のサイズ:
┌──────────────────────────────┐  オフセット  サイズ
│ Field: string (16バイト)     │  0          16バイト
│   ptr: *byte (8バイト)       │  0          8バイト
│   len: int (8バイト)         │  8          8バイト
├──────────────────────────────┤
│ Value: interface{} (16バイト)│  16         16バイト
│   tab: *itab (8バイト)       │  16         8バイト
│   data: *value (8バイト)     │  24         8バイト
├──────────────────────────────┤
│ Message: string (16バイト)   │  32         16バイト
│   ptr: *byte (8バイト)       │  32         8バイト
│   len: int (8バイト)         │  40         8バイト
└──────────────────────────────┘
合計: 48バイト

error インターフェースとして返される時:
┌──────────────────────────────┐
│ tab: *itab                   │  ← (*ValidationError, error) の itab
├──────────────────────────────┤
│ data: *ValidationError       │  → ヒープ上の ValidationError (48バイト)
└──────────────────────────────┘

複雑なカスタムエラー - HTTPError

type HTTPError struct {
    StatusCode int
    Method     string
    URL        string
    Err        error  // 元のエラーをラップ
    Timestamp  time.Time
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("[%d] %s %s: %v (at %s)",
        e.StatusCode, e.Method, e.URL, e.Err,
        e.Timestamp.Format(time.RFC3339))
}

// Unwrap メソッドでエラーチェーンをサポート
func (e *HTTPError) Unwrap() error {
    return e.Err
}

メモリレイアウト:

HTTPError 構造体:
┌──────────────────────────────┐  オフセット  サイズ
│ StatusCode: int (8バイト)    │  0          8バイト
├──────────────────────────────┤
│ Method: string (16バイト)    │  8          16バイト
│   ptr: *byte                 │  8          8バイト
│   len: int                   │  16         8バイト
├──────────────────────────────┤
│ URL: string (16バイト)       │  24         16バイト
│   ptr: *byte                 │  24         8バイト
│   len: int                   │  32         8バイト
├──────────────────────────────┤
│ Err: error (16バイト)        │  40         16バイト  ← ネストしたエラー
│   tab: *itab                 │  40         8バイト
│   data: *error               │  48         8バイト
├──────────────────────────────┤
│ Timestamp: time.Time (24B)   │  56         24バイト
│   wall: uint64               │  56         8バイト
│   ext: int64                 │  64         8バイト
│   loc: *Location             │  72         8バイト
└──────────────────────────────┘
合計: 80バイト

階層構造:
error interface (16B)
  ↓
HTTPError (80B)
  ↓ Err field
error interface (16B)
  ↓
元のエラー (可変サイズ)

🔑 Unwrap パターン: Unwrap() メソッドを実装することで、エラーチェーンを作成できます。

エラー処理のパフォーマンス最適化

1. センチネルエラーの活用

// 遅い: 毎回ヒープ割り当て
func validate(value int) error {
    if value < 0 {
        return errors.New("value must be positive")  // ヒープ割り当て
    }
    return nil
}

// 速い: 事前定義されたエラー
var ErrNegativeValue = errors.New("value must be positive")

func validateFast(value int) error {
    if value < 0 {
        return ErrNegativeValue  // ポインタコピーのみ
    }
    return nil
}

ベンチマーク:

BenchmarkValidate-8      5000000    250 ns/op    32 B/op    1 allocs/op
BenchmarkValidateFast-8  50000000   25 ns/op     0 B/op     0 allocs/op

パフォーマンス改善: 10倍
メモリ削減: 100%

💡 10倍の改善: センチネルエラーは、ヒープ割り当てを回避できます。

2. エラーラッピングの削減

// 遅い: 多段階ラッピング
func processDeep() error {
    err := level1()
    if err != nil {
        return fmt.Errorf("level 5: %w",
            fmt.Errorf("level 4: %w",
                fmt.Errorf("level 3: %w",
                    fmt.Errorf("level 2: %w", err))))
    }
    return nil
}

// 速い: 必要な箇所のみラッピング
func processShallow() error {
    err := level1()
    if err != nil {
        return fmt.Errorf("process failed: %w", err)
    }
    return nil
}

メモリ影響:

多段階ラッピング:
- 4つの wrapError 構造体: 32バイト × 4 = 128バイト
- 4つの文字列: ~100バイト
- 合計: ~230バイト + 元のエラー

単一ラッピング:
- 1つの wrapError 構造体: 32バイト
- 1つの文字列: ~25バイト
- 合計: ~60バイト + 元のエラー

メモリ削減: 約75%

3. エラーチェックの最適化

// 遅い: errors.Is を複数回呼び出し
func handleError(err error) {
    if errors.Is(err, fs.ErrNotExist) {
        // ...
    } else if errors.Is(err, fs.ErrPermission) {
        // ...
    } else if errors.Is(err, fs.ErrExist) {
        // ...
    }
}

// 速い: 型アサーションを使用
func handleErrorFast(err error) {
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        // syscall.Errno による switch
        if errno, ok := pathErr.Err.(syscall.Errno); ok {
            switch errno {
            case syscall.ENOENT:
                // ファイルが存在しない
            case syscall.EACCES:
                // 権限エラー
            case syscall.EEXIST:
                // 既に存在
            }
            return
        }
    }
}

パフォーマンス比較:

handleError:
- errors.Is の呼び出し: 3回
- エラーチェーンの走査: 最大 3 × チェーン長
- コスト: O(3n) (n = チェーン長)

handleErrorFast:
- errors.As の呼び出し: 1回
- エラーチェーンの走査: 1回
- switch による分岐: O(1)
- コスト: O(n)

改善: 3倍高速

4. エラー文字列の遅延生成

// 遅い: 常にフォーマット
type SlowError struct {
    operation string
    details   string
}

func (e *SlowError) Error() string {
    // 毎回フォーマット
    return fmt.Sprintf("operation %s failed: %s", e.operation, e.details)
}

// 速い: 必要な時だけフォーマット
type FastError struct {
    operation string
    details   string
    msg       string  // キャッシュ
    once      sync.Once
}

func (e *FastError) Error() string {
    e.once.Do(func() {
        e.msg = fmt.Sprintf("operation %s failed: %s", e.operation, e.details)
    })
    return e.msg
}

ベンチマーク:

BenchmarkSlowError-8  1000000   1200 ns/op   64 B/op   2 allocs/op
BenchmarkFastError-8  10000000  120 ns/op    0 B/op    0 allocs/op
(2回目以降の Error() 呼び出し)

改善: 10倍高速(複数回呼び出す場合)

5. エラープール

// 大量のエラーを扱う場合
var errorPool = sync.Pool{
    New: func() interface{} {
        return &DetailedError{}
    },
}

type DetailedError struct {
    Code    int
    Message string
    Details map[string]interface{}
}

func (e *DetailedError) Error() string {
    return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Details)
}

func (e *DetailedError) Reset() {
    e.Code = 0
    e.Message = ""
    e.Details = nil
}

func NewDetailedError(code int, msg string) *DetailedError {
    err := errorPool.Get().(*DetailedError)
    err.Code = code
    err.Message = msg
    return err
}

func ReleaseDetailedError(err *DetailedError) {
    err.Reset()
    errorPool.Put(err)
}

// 使用例
func processRequest(data []byte) error {
    if len(data) == 0 {
        return NewDetailedError(400, "empty data")
    }
    return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    data, _ := io.ReadAll(r.Body)

    err := processRequest(data)
    if err != nil {
        handleError(w, err)

        // プールに返却
        if detailedErr, ok := err.(*DetailedError); ok {
            ReleaseDetailedError(detailedErr)
        }
        return
    }
}

ベンチマーク:

BenchmarkNewError-8       5000000   250 ns/op   64 B/op   1 allocs/op
BenchmarkPooledError-8    20000000  60 ns/op    0 B/op    0 allocs/op

改善: 4倍高速
メモリ削減: 100%(定常状態)

実践的なエラー処理パターン

1. リトライ可能エラー

type RetryableError struct {
    Err        error
    RetryAfter time.Duration
}

func (e *RetryableError) Error() string {
    return fmt.Sprintf("retryable error: %v (retry after %v)", e.Err, e.RetryAfter)
}

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

// リトライロジック
func withRetry(fn func() error, maxRetries int) error {
    var err error
    for i := 0; i < maxRetries; i++ {
        err = fn()
        if err == nil {
            return nil
        }

        var retryErr *RetryableError
        if errors.As(err, &retryErr) {
            time.Sleep(retryErr.RetryAfter)
            continue
        }

        // リトライ不可能なエラー
        return err
    }
    return fmt.Errorf("max retries exceeded: %w", err)
}

2. エラーアグリゲーション

type ErrorList struct {
    Errors []error
}

func (e *ErrorList) Error() string {
    if len(e.Errors) == 0 {
        return "no errors"
    }
    if len(e.Errors) == 1 {
        return e.Errors[0].Error()
    }
    return fmt.Sprintf("%d errors occurred: %v", len(e.Errors), e.Errors[0])
}

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

func (e *ErrorList) Err() error {
    if len(e.Errors) == 0 {
        return nil
    }
    return e
}

// 使用例
func processMultipleFiles(files []string) error {
    var errs ErrorList

    for _, file := range files {
        if err := processFile(file); err != nil {
            errs.Add(fmt.Errorf("failed to process %s: %w", file, err))
        }
    }

    return errs.Err()
}

3. コンテキスト付きエラー

type ContextError struct {
    Err     error
    Context map[string]interface{}
}

func (e *ContextError) Error() string {
    return fmt.Sprintf("%v (context: %v)", e.Err, e.Context)
}

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

func WithContext(err error, key string, value interface{}) error {
    var ctxErr *ContextError
    if errors.As(err, &ctxErr) {
        // 既存のコンテキストに追加
        ctxErr.Context[key] = value
        return ctxErr
    }

    // 新しいコンテキストエラーを作成
    return &ContextError{
        Err: err,
        Context: map[string]interface{}{
            key: value,
        },
    }
}

// 使用例
func processUser(userID int) error {
    user, err := fetchUser(userID)
    if err != nil {
        return WithContext(err, "userID", userID)
    }

    if err := validateUser(user); err != nil {
        err = WithContext(err, "userID", userID)
        err = WithContext(err, "username", user.Name)
        return err
    }

    return nil
}

4. エラースタックトレース

type StackError struct {
    Err   error
    Stack []uintptr
}

func (e *StackError) Error() string {
    return e.Err.Error()
}

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

func (e *StackError) StackTrace() string {
    frames := runtime.CallersFrames(e.Stack)
    var buf strings.Builder

    for {
        frame, more := frames.Next()
        fmt.Fprintf(&buf, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
        if !more {
            break
        }
    }

    return buf.String()
}

func NewStackError(err error) error {
    const depth = 32
    var pcs [depth]uintptr
    n := runtime.Callers(2, pcs[:])

    return &StackError{
        Err:   err,
        Stack: pcs[0:n],
    }
}

// 使用例
func riskyOperation() error {
    if err := doSomething(); err != nil {
        return NewStackError(fmt.Errorf("risky operation failed: %w", err))
    }
    return nil
}

🔍 自己確認問題

問題1: error インターフェースのメモリサイズ

次のコードの出力を予測してください:

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return e.Message
}

func main() {
    var err error = &MyError{Code: 404, Message: "Not Found"}

    fmt.Println(unsafe.Sizeof(err))
    fmt.Println(unsafe.Sizeof(MyError{}))
}

解答

出力(64ビットシステム):

16
24

説明:

  • unsafe.Sizeof(err): error インターフェースは常に16バイト(tab + data ポインタ)
  • unsafe.Sizeof(MyError{}): int(8) + string(16) = 24バイト

メモリレイアウト:

err (interface): 16バイト
┌────────────────┐
│ tab: 8バイト   │
│ data: 8バイト  │
└────────────────┘

MyError: 24バイト
┌────────────────────┐
│ Code: 8バイト      │
│ Message: 16バイト  │
│   ptr: 8バイト     │
│   len: 8バイト     │
└────────────────────┘

問題2: errors.Is の動作

次のコードの出力を予測してください:

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

func fetchData() error {
    return fmt.Errorf("failed to fetch: %w",
        fmt.Errorf("database error: %w", ErrNotFound))
}

func main() {
    err := fetchData()

    fmt.Println(err == ErrNotFound)
    fmt.Println(errors.Is(err, ErrNotFound))
}

解答

出力:

false
true

説明:

  • err == ErrNotFound: false(err はラップされたエラー、直接比較では一致しない)
  • errors.Is(err, ErrNotFound): true(エラーチェーンをたどって ErrNotFound を見つける)

エラーチェーン:

err:
  wrapError{"failed to fetch: ..."}
    ↓ Unwrap
  wrapError{"database error: ..."}
    ↓ Unwrap
  ErrNotFound ← errors.Is がここまでたどる

問題3: errors.As の使用

次のコードを完成させて、エラーから os.PathError を取得してください:

func processFile(name string) error {
    _, err := os.Open(name)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    return nil
}

func main() {
    err := processFile("nonexistent.txt")

    // ここを実装:PathError からファイル名と操作を取得
}

解答

func main() {
    err := processFile("nonexistent.txt")

    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Printf("Operation: %s\n", pathErr.Op)
        fmt.Printf("Path: %s\n", pathErr.Path)
        fmt.Printf("Error: %v\n", pathErr.Err)
    } else {
        fmt.Println("Not a path error")
    }
}

出力例:

Operation: open
Path: nonexistent.txt
Error: no such file or directory

メモリ操作:

処理前: pathErr = nil
errors.As 実行中:
  1. エラーチェーンを走査
  2. *os.PathError 型を発見
  3. &pathErr に代入
処理後: pathErr → os.PathError インスタンス

問題4: panic と recover の動作

次のコードの出力を予測してください:

func example() {
    defer fmt.Println("defer 1")
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered: %v\n", r)
        }
    }()
    defer fmt.Println("defer 3")

    panic("oops")
}

func main() {
    example()
    fmt.Println("After example")
}

解答

出力:

defer 3
Recovered: oops
defer 1
After example

実行順序:

1. panic("oops") 発生
2. defer が逆順に実行される:
   a. defer 3 → "defer 3" 出力
   b. defer recover() → panic を回復、"Recovered: oops" 出力
   c. defer 1 → "defer 1" 出力
3. panic が回復したので、通常の実行フローに戻る
4. "After example" 出力

問題5: エラーラッピングの最適化

次の2つの実装のうち、どちらが速いですか?

// 実装1
func validate1(value int) error {
    if value < 0 {
        return errors.New("value must be positive")
    }
    return nil
}

// 実装2
var ErrNegative = errors.New("value must be positive")

func validate2(value int) error {
    if value < 0 {
        return ErrNegative
    }
    return nil
}

解答

validate2 の方が大幅に高速です。

理由:

  • validate1: 毎回 errors.New がヒープ割り当てを行う(~32バイト)
  • validate2: 事前に確保されたエラーを返すだけ(ポインタコピーのみ)

ベンチマーク:

BenchmarkValidate1-8    10000000    150 ns/op    32 B/op    1 allocs/op
BenchmarkValidate2-8    100000000   15 ns/op     0 B/op     0 allocs/op

改善: 10倍高速、メモリ割り当てなし

問題6: カスタム Is メソッドの実装

次の要件を満たすカスタムエラー型を実装してください:

  • すべてのインスタンスが同じセンチネルエラーと一致する
  • エラーメッセージにタイムスタンプを含む

解答

type TimeoutError struct {
    Timestamp time.Time
    Operation string
}

func (e *TimeoutError) Error() string {
    return fmt.Sprintf("timeout at %s during %s",
        e.Timestamp.Format(time.RFC3339), e.Operation)
}

// カスタム Is メソッド
func (e *TimeoutError) Is(target error) bool {
    _, ok := target.(*TimeoutError)
    return ok
}

// センチネルエラー
var ErrTimeout = &TimeoutError{}

// 使用例
func doRequest() error {
    return &TimeoutError{
        Timestamp: time.Now(),
        Operation: "HTTP request",
    }
}

func main() {
    err := doRequest()

    // どの TimeoutError インスタンスも ErrTimeout と一致
    if errors.Is(err, ErrTimeout) {
        fmt.Println("Timeout occurred")
    }
}

メモリ構造:

ErrTimeout (センチネル):
┌────────────────────────┐
│ TimeoutError:          │
│   Timestamp: 時刻なし  │
│   Operation: ""        │
└────────────────────────┘

実際のエラー:
┌────────────────────────┐
│ TimeoutError:          │
│   Timestamp: 2024-...  │
│   Operation: "HTTP..." │
└────────────────────────┘

errors.Is でカスタム Is が呼ばれる
→ 型が TimeoutError なら true

問題7: エラーチェーンの可視化

次のコードのエラーチェーンを可視化する関数を実装してください:

func printErrorChain(err error) {
    // ここを実装:エラーチェーンを階層的に出力
}

func main() {
    err := fmt.Errorf("level 3: %w",
        fmt.Errorf("level 2: %w",
            errors.New("level 1: original error")))

    printErrorChain(err)
}

期待する出力:

[0] level 3: level 2: level 1: original error
[1] level 2: level 1: original error
[2] level 1: original error

解答

func printErrorChain(err error) {
    level := 0
    for err != nil {
        fmt.Printf("[%d] %v\n", level, err)
        err = errors.Unwrap(err)
        level++
    }
}

// より詳細なバージョン
func printErrorChainDetailed(err error) {
    level := 0
    for err != nil {
        indent := strings.Repeat("  ", level)
        fmt.Printf("%s[%d] Type: %T\n", indent, level, err)
        fmt.Printf("%s    Message: %v\n", indent, err)

        err = errors.Unwrap(err)
        level++
    }
}

// グラフィカルな出力
func printErrorChainGraphical(err error) {
    level := 0
    for err != nil {
        if level > 0 {
            fmt.Println("    ↓ Unwrap")
        }
        fmt.Printf("[Level %d] %T\n", level, err)
        fmt.Printf("Message: %v\n", err)

        err = errors.Unwrap(err)
        level++
    }
}

出力例:

[Level 0] *fmt.wrapError
Message: level 3: level 2: level 1: original error
    ↓ Unwrap
[Level 1] *fmt.wrapError
Message: level 2: level 1: original error
    ↓ Unwrap
[Level 2] *errors.errorString
Message: level 1: original error

問題8: カスタム As メソッドの実装

複数のエラーを保持する MultiError 型に As メソッドを実装してください:

type MultiError struct {
    Errors []error
}

func (e *MultiError) Error() string {
    return fmt.Sprintf("%d errors occurred", len(e.Errors))
}

// As メソッドを実装

解答

type MultiError struct {
    Errors []error
}

func (e *MultiError) Error() string {
    if len(e.Errors) == 0 {
        return "no errors"
    }
    return fmt.Sprintf("%d errors occurred: %v", len(e.Errors), e.Errors[0])
}

// カスタム As メソッド
func (e *MultiError) As(target interface{}) bool {
    // 各エラーに対して As を試行
    for _, err := range e.Errors {
        if errors.As(err, target) {
            return true
        }
    }
    return false
}

// Go 1.20+ の複数エラー対応
func (e *MultiError) Unwrap() []error {
    return e.Errors
}

// 使用例
func processFiles(files []string) error {
    var errs []error

    for _, file := range files {
        _, err := os.Open(file)
        if err != nil {
            errs = append(errs, err)
        }
    }

    if len(errs) > 0 {
        return &MultiError{Errors: errs}
    }

    return nil
}

func main() {
    err := processFiles([]string{"file1.txt", "file2.txt", "file3.txt"})

    // 最初に見つかった PathError を取得
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Printf("Found path error: %s\n", pathErr.Path)
    }

    // 複数のエラーをチェック
    if errors.Is(err, fs.ErrNotExist) {
        fmt.Println("Some files not found")
    }
}

問題9: エラーハンドリングの設計

データベースアクセス層のエラーハンドリングを設計してください。要件は以下の通りです:

  • 接続エラー
  • クエリエラー
  • レコードが見つからない
  • 一意制約違反
  • リトライ可能エラーの判定

解答

package database

import (
    "errors"
    "fmt"
    "time"
)

// センチネルエラー
var (
    ErrConnection   = errors.New("database connection error")
    ErrQuery        = errors.New("query execution error")
    ErrNotFound     = errors.New("record not found")
    ErrDuplicate    = errors.New("duplicate key violation")
)

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

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

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

// リトライ可能判定
func (e *DBError) IsRetryable() bool {
    return errors.Is(e.Err, ErrConnection)
}

// データベース層
type DB struct {
    // ...
}

func (db *DB) FindUser(id int) (*User, error) {
    user, err := db.query("SELECT * FROM users WHERE id = ?", id)
    if err != nil {
        // 接続エラー
        if isConnectionError(err) {
            return nil, &DBError{
                Op:    "find",
                Table: "users",
                Err:   fmt.Errorf("%w: %v", ErrConnection, err),
            }
        }

        // レコードが見つからない
        if isNoRowsError(err) {
            return nil, &DBError{
                Op:    "find",
                Table: "users",
                Err:   ErrNotFound,
            }
        }

        // その他のエラー
        return nil, &DBError{
            Op:    "find",
            Table: "users",
            Err:   fmt.Errorf("%w: %v", ErrQuery, err),
        }
    }

    return user, nil
}

func (db *DB) CreateUser(user *User) error {
    err := db.insert("INSERT INTO users ...", user)
    if err != nil {
        // 一意制約違反
        if isDuplicateError(err) {
            return &DBError{
                Op:    "create",
                Table: "users",
                Err:   fmt.Errorf("%w: %v", ErrDuplicate, err),
            }
        }

        return &DBError{
            Op:    "create",
            Table: "users",
            Err:   fmt.Errorf("%w: %v", ErrQuery, err),
        }
    }

    return nil
}

// アプリケーション層
func GetUserWithRetry(db *DB, id int, maxRetries int) (*User, error) {
    var err error
    for i := 0; i < maxRetries; i++ {
        user, err := db.FindUser(id)
        if err == nil {
            return user, nil
        }

        // DBError にアサート
        var dbErr *DBError
        if errors.As(err, &dbErr) && dbErr.IsRetryable() {
            time.Sleep(time.Second * time.Duration(i+1))
            continue
        }

        // リトライ不可能
        return nil, err
    }

    return nil, fmt.Errorf("max retries exceeded: %w", err)
}

// エラーハンドリング
func HandleError(err error) int {
    if errors.Is(err, ErrNotFound) {
        return 404
    }

    if errors.Is(err, ErrDuplicate) {
        return 409
    }

    var dbErr *DBError
    if errors.As(err, &dbErr) {
        if dbErr.IsRetryable() {
            return 503
        }
    }

    return 500
}

設計のポイント:

  • センチネルエラーで大分類
  • カスタムエラー型で詳細情報
  • Unwrap でエラーチェーン
  • IsRetryable でリトライ判定
  • 層ごとにエラーを変換

問題10: エラーのメモリ効率

大量のエラーを扱う場合の最適化を実装してください(1秒間に10000回呼ばれる関数):

func processRequest(data []byte) error {
    if len(data) == 0 {
        return errors.New("empty data")  // ヒープ割り当て!
    }
    // 処理...
    return nil
}

解答

最適化アプローチ:

// 方法1: センチネルエラー(最もシンプル)
var ErrEmptyData = errors.New("empty data")

func processRequestV1(data []byte) error {
    if len(data) == 0 {
        return ErrEmptyData  // ポインタコピーのみ
    }
    return nil
}

// 方法2: エラーコード(型安全)
type ErrorCode int

const (
    ErrNone ErrorCode = iota
    ErrEmptyData
    ErrInvalidFormat
    ErrTooLarge
)

func (e ErrorCode) Error() string {
    switch e {
    case ErrEmptyData:
        return "empty data"
    case ErrInvalidFormat:
        return "invalid format"
    case ErrTooLarge:
        return "data too large"
    default:
        return "unknown error"
    }
}

func processRequestV2(data []byte) error {
    if len(data) == 0 {
        return ErrEmptyData  // int の変換のみ
    }
    return nil
}

// 方法3: エラープール(詳細情報が必要な場合)
var errorPool = sync.Pool{
    New: func() interface{} {
        return &DetailedError{}
    },
}

type DetailedError struct {
    Code    ErrorCode
    Message string
    Details map[string]interface{}
}

func (e *DetailedError) Error() string {
    return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Details)
}

func (e *DetailedError) Reset() {
    e.Code = ErrNone
    e.Message = ""
    e.Details = nil
}

func NewDetailedError(code ErrorCode, msg string) *DetailedError {
    err := errorPool.Get().(*DetailedError)
    err.Code = code
    err.Message = msg
    return err
}

func ReleaseDetailedError(err *DetailedError) {
    err.Reset()
    errorPool.Put(err)
}

func processRequestV3(data []byte) error {
    if len(data) == 0 {
        return NewDetailedError(ErrEmptyData, "received empty request")
    }
    return nil
}

// 使用例
func handler(w http.ResponseWriter, r *http.Request) {
    data, _ := io.ReadAll(r.Body)

    err := processRequestV3(data)
    if err != nil {
        handleError(w, err)

        // プールに返却
        if detailedErr, ok := err.(*DetailedError); ok {
            ReleaseDetailedError(detailedErr)
        }
        return
    }
}

ベンチマーク比較:

BenchmarkOriginal-8     5000000    250 ns/op    32 B/op    1 allocs/op
BenchmarkV1Sentinel-8   50000000   25 ns/op     0 B/op     0 allocs/op
BenchmarkV2ErrorCode-8  50000000   25 ns/op     0 B/op     0 allocs/op
BenchmarkV3Pool-8       20000000   60 ns/op     0 B/op     0 allocs/op

選択基準:

  • 単純なエラー → センチネルエラー(V1)
  • エラーコードで十分 → ErrorCode(V2)
  • 詳細情報が必要 → エラープール(V3)
  • 複雑なコンテキスト → カスタムエラー型(ヒープ割り当て許容)

メモリ使用量の比較:

元の実装:
- 10000 req/sec × 32 bytes = 320 KB/sec
- 1分間で ~19 MB のGC圧力

センチネルエラー:
- 0 bytes/sec
- GC圧力なし

改善: 100% メモリ削減

まとめ

この章では、Goのエラー処理をマシンレベルで深く理解しました。

🔑 重要ポイント:

  • error インターフェース:
- 16バイトの構造(tab + data ポインタ) - tab: 型情報と メソッドテーブルを含む itab - data: 実際のエラーデータへのポインタ

  • errors.New と fmt.Errorf:
- errors.New: ヒープに errorString を割り当て - fmt.Errorf の %w: wrapError を作成してエラーチェーン構築 - %v: 単純なフォーマット(Unwrap 不可)

  • エラーラッピング:
- wrapError 構造体でメッセージと元のエラーを保持 - Unwrap() でチェーンをたどれる - メモリコスト: 32バイト/レベル + 文字列

  • errors.Is:
- エラーチェーンを線形探索(O(n)) - 直接比較 → カスタム Is → Unwrap の順で処理 - センチネルエラーとの比較に使用

  • errors.As:
- エラーチェーンを走査して型マッチング - リフレクションで型変換と代入 - ラップされたカスタムエラーの取得に使用

  • panic/recover:
- _panic 構造体でパニック状態を管理 - defer チェーンを逆順に実行 - recover() でパニックを捕捉し、通常フローに復帰 - コスト: ~10-50µs(通常のエラー処理より遅い)

💡 パフォーマンスのヒント:

  • センチネルエラーを活用してヒープ割り当てを削減(10倍高速)
  • 必要な箇所でのみエラーをラップ(75%メモリ削減)
  • エラーチェックを最適化(型アサーション活用で3倍高速)
  • 大量のエラーを扱う場合はエラープールを検討
  • エラー文字列の遅延生成(複数回呼び出しで10倍高速)

⚠️ よくある落とし穴:

  • nil インターフェースと nil ポインタの混同
  • == 比較の代わりに errors.Is を使う
  • 型アサーションの代わりに errors.As を使う
  • 不必要な多段階ラッピング
  • エラー文字列の大文字開始(小文字で始める)
  • panic を通常のエラー処理に使用する

ベストプラクティス:

  • エラーは早期に処理する(early return)
  • 適切なコンテキストを追加する
  • センチネルエラーとカスタムエラーを使い分ける
  • エラーをラップして情報を保持する(%w 使用)
  • errors.Iserrors.As を活用する
  • panic は本当の例外時のみ使用する(回復不可能な状況)

これでGo Foundationsコースのエラー処理は完了です!