panicとrecoverの使い方をレビューでどう評価するか?

Go言語では、基本的なエラーハンドリングはerror型の返却によって行います。ところが一部の例外的状況では、panicrecoverを用いた異常制御が利用されます。
これらは便利な仕組みである反面、設計意図・責務境界・回復設計の曖昧さを生みやすいため、レビューアーは慎重にその適用理由を読み解く必要があります。

本記事では、panic/recoverを使った設計の「良い例」と「良くない例」を提示し、レビュー時にどのようなポイントに着目するべきかを整理していきます。


正しいpanic/recover利用の設計例と解説

ケース背景

あるデータパイプライン処理で、入力検証は既に別途完了しており、基本的に発生しないはずの論理破綻が想定されています。
ただし、もし内部整合性が崩れた場合はpanicによって異常を通知し、呼び出し元がrecoverを通じてシステムの全体崩壊を防止する設計です。

package main

import (
    "fmt"
    "log"
)

func main() {
    SafeExecute(func() {
        ProcessData(-5)
    })
}

func SafeExecute(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Recovered from panic: %v", r)
        }
    }()
    fn()
}

func ProcessData(n int) {
    validateInput(n)
    fmt.Println("Processing:", n)
}

func validateInput(n int) {
    if n < 0 {
        panic(fmt.Sprintf("invalid input: %d must be positive", n))
    }
}

この設計の技術解説

  • panicは明示的な前提条件違反検出に限定
    validateInput()でのpanicは、事前検証を通過している前提が崩れた場合のみ発生します。通常のエラーではなく、論理バグを示す契約違反として利用されています。

  • panic発生はSafeExecute内に閉じ込められている
    呼び出し元が安全実行ラッパー SafeExecute を利用しており、panicが漏れ出る範囲が制御されています。

  • recoverブロックはログ出力・障害検知を含む
    recover時にログ出力が行われ、問題発生をサイレントに握り潰す実装にはなっていません。

  • 通常のエラー制御(error return)と混在しない構造
    panicを使用する箇所と通常エラーハンドリングは明確に分離され、読み手が制御構造を追いやすくなっています。

panic/recoverの使用は「構造的な異常設計」として成立しているかをレビューの基準にします。ライブラリ・フレームワーク層で責務を閉じ込める設計は合理的です。


良くないpanic/recover実装例

次はレビュー対象となる問題例を提示します。

package main

import (
    "fmt"
)

func main() {
    ProcessData("10")
}

func ProcessData(input string) {
    defer func() {
        recover()
    }()

    value := parse(input)
    fmt.Println("Processed value:", value)
}

func parse(s string) int {
    var result int
    _, err := fmt.Sscanf(s, "%d", &result)
    if err != nil {
        panic("parse failed")
    }
    return result
}

問題点をレビューアーが指摘する

func ProcessData(input string) {
    defer func() {
        recover()
    }()
@Reviewer
recoverだけで握り潰しており、障害の記録・通知が行われません。panic発生箇所の原因把握ができず、障害調査が困難になります。最低限ログ出力を追加してください。
value := parse(input) fmt.Println("Processed value:", value) } func parse(s string) int { var result int _, err := fmt.Sscanf(s, "%d", &result) if err != nil { panic("parse failed") }
@Reviewer
単なる入力エラーをpanicにしています。通常の入力バリデーション失敗はpanicでなくerror returnを用いてください。panicは契約違反や論理破綻時のみ使います。
return result }

panic/recover設計レビューの観点整理

観点別チェックリスト

観点カテゴリ 確認すべき質問例
利用意図 panicは契約違反・プログラム破綻限定になっているか?
recover責務 recoverは記録(ログ出力等)を伴っているか?握り潰していないか?
通常制御との分離 panic使用箇所と通常のerror制御が分離されているか?
スコープの限定 panicがシステム全体に伝播しない設計になっているか?
障害調査性 障害解析可能な情報(ログ・メトリクス)は残されているか?
再利用性 recover処理を汎用関数等に切り出せているか?

panicは「バグ検出の最後の砦」として狭く使う。recoverは「握り潰しでなく、可視化・通知の一手段」として評価する。
この責務の分離がレビュー観点の核心です。


あとがき

panic/recoverはGoの例外制御としては限定用途であり、ほとんどのエラー処理はerror型で記述されるべきという原則は揺らぎません。
panicを見かけたとき、レビューアーは単に「使っている/使っていない」ではなく、その設計責務を以下の観点で立ち止まって読み解く必要があります。

  • なぜpanicが必要だったのか
  • どこまで責務を閉じ込められているのか
  • 予期せぬ握り潰しや調査困難を生んでいないか

panic/recoverレビューはコード全体の構造設計レビューの一部です。責務境界、可視性、可読性、安全性──その全てが問われます。