コンテンツにスキップ

Go の panic / defer / recover の動作を理解する

3つの仕組みの関係

panic  → goroutineのスタックを巻き戻しながら各フレームのdeferを実行
defer  → 関数が終了する直前に必ず実行される(panicでも)
recover → deferの中でpanicを捕捉して通常処理に戻す

panic

実行を即座に中断し、goroutineのスタックを巻き戻す。

func main() {
    fmt.Println("start")
    panic("something went wrong")  // ここで停止
    fmt.Println("never reached")   // 実行されない
}

nilポインタ参照、範囲外アクセスなどもruntime panicとして発生する。

defer

関数が終了するタイミング(正常終了・panic・return)で実行されることが保証される。

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // どのreturnパスでも必ずClose

    // ... 処理
    return nil
}

deferはLIFO(後入れ先出し) で実行される。

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}
// 出力: 3, 2, 1

よくある罠: ループ内のdefer

// NG: ループが終わるまでCloseされない
for _, path := range paths {
    f, _ := os.Open(path)
    defer f.Close() // ← 関数が終わるまで溜まり続ける
}

// OK: クロージャに切り出す
for _, path := range paths {
    func() {
        f, _ := os.Open(path)
        defer f.Close() // ← この無名関数が終わるとすぐClose
    }()
}

recover

defer の中で呼ぶと、panicを捕捉してpanicの値を返す。 recoverが呼ばれるとスタックの巻き戻しが停止し、通常実行に戻る。

func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()

    return a / b, nil // b=0ならpanic
}

func main() {
    result, err := safeDiv(10, 0)
    fmt.Println(result, err) // 0, recovered from panic: runtime error: integer divide by zero
}

recover が効かないケース

  • defer の外で呼んでも nil を返すだけ(panicは止まらない)
  • 別goroutineのpanicは捕捉できない
go func() {
    panic("goroutine panic") // ← main goroutineのrecoverでは捕捉不可
}()

実行順序の完全な例

func example() {
    defer fmt.Println("defer 1")
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    defer fmt.Println("defer 3")

    panic("boom")
    fmt.Println("after panic") // 実行されない
}
// 出力:
// defer 3
// recovered: boom
// defer 1

panicが発生するとdeferがLIFOで実行される。recoverを含むdeferがpanicを捕捉すると、残りのdeferも続けて実行される。

いつ使うか

  • panic: プログラムの不変条件が壊れた場合(バグ)。通常のエラー処理にはerrorを返す
  • defer: リソース解放(Close、Unlock)、ログ、タイミング計測
  • recover: ライブラリの境界でpanicを封じ込める(HTTPハンドラのミドルウェアなど)
// HTTPサーバーでpanicを500エラーに変換するミドルウェア
func recoverMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal Server Error", 500)
                log.Printf("panic recovered: %v", r)
            }
        }()
        next.ServeHTTP(w, r)
    })
}