Go の panic と recover
Go には通常のエラーハンドリング(error を返す)とは別に、panic と recover という仕組みがある。これは回復不能な異常事態に対処するための機能だ。
panic とは
panic はプログラムの実行を中断し、スタックを巻き戻しながら終了する。通常のエラーでは対処できない致命的な状況で使う。
func mustParseInt(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("invalid integer: %s", s))
}
return n
}panic が発生すると、現在の関数の実行が停止し、defer された関数を順に実行しながらスタックを巻き戻す。最終的にプログラムがクラッシュしてスタックトレースが表示される。
panic が発生するケース
Go ランタイムが自動的に panic を発生させるケースもある。
nil のポインタに対してメソッドを呼んだりフィールドにアクセスしたりすると panic が発生する。
スライスや配列の範囲外インデックスにアクセスすると panic が発生する。
ok を受け取らない型アサーションで、実際の型が異なると panic になる。
var s []int
_ = s[0] // panic: index out of range
var x interface{} = "hello"
_ = x.(int) // panic: interface conversionrecover による回復
recover は panic から回復するための関数だ。defer された関数の中でのみ有効に機能する。
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
f()
return nil
}recover は panic が発生していれば panic に渡された値を返し、発生していなければ nil を返す。recover を呼ぶと panic の伝播が停止し、プログラムの実行が継続される。
panic と recover の使用例
HTTP サーバーでリクエストごとに recover することで、1つのリクエストの panic が他に影響しないようにできる。
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}これにより、ハンドラ内で panic が起きてもサーバー全体は停止せず、500エラーを返して処理を継続できる。
panic を使うべきとき
プログラムの前提条件が崩れた場合。初期化時の設定エラーや、あり得ないはずの状態など。
予測可能な失敗。ファイルが見つからない、ネットワークエラー、バリデーション失敗など。
ライブラリでは基本的に panic ではなく error を返すべきだ。呼び出し側に回復の選択肢を与えるためである。
Must パターン
初期化時など、エラーが致命的な場面では Must プレフィックスの関数がよく使われる。
func MustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(err)
}
return re
}
// パッケージレベルで初期化
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)標準ライブラリの regexp.MustCompile や template.Must がこのパターンを採用している。初期化時のエラーは修正なしには回復できないため、panic で即座に失敗させるのが合理的だ。