panicはどう見えるか
panicは現在の関数を止めてそのdeferした呼び出しを実行し、次にその呼び出し元で同じことをし、というようにスタックを上っていきます。ゴルーチンの一番上まで到達すると、プログラムはクラッシュします。
このプログラムはステータス2で終了します。出力は次のとおりです。
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
deferした呼び出しは、クラッシュの報告の前に実行されました。トレースにはゴルーチン、関数、行が示されており、たいていはそれでバグを見つけられます。
よくある実行時panic
| メッセージ | 原因 |
|---|---|
index out of range [5] with length 3 | スライス、配列、文字列の末尾を超えたインデックス |
slice bounds out of range [:7] with capacity 5 | 容量を超えたスライス操作 |
invalid memory address or nil pointer dereference | nilポインタを通したフィールドの読み取りや呼び出し |
assignment to entry in nil map | 作成されていないマップへの書き込み |
interface conversion: interface {} is int, not string | 間違った型への1値の型アサーション |
integer divide by zero | 0による整数の除算や剰余(浮動小数点数なら代わりに +Inf や NaN) |
close of closed channel、send on closed channel | チャネルの誤用 |
all goroutines are asleep - deadlock! | すべてのゴルーチンがブロックしている(panicではなく致命的エラー) |
どれも、処理すべき状態ではなくプログラムのバグです。直し方は recover ではなく、範囲チェック、nilチェック、make、カンマokのアサーションです。
回復する
recover() はpanicを止めます。panicが巻き戻っている間に実行されるコードはdeferした関数だけなので、deferした関数の中で直接呼んだときにしか機能しません。
出力:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
2回目の呼び出しで起きたことは次のとおりです。
a / bがpanicした。- deferしたクロージャが実行され、
recover()がpanicの値(runtime.Error)を返した。 - 巻き戻しが止まった。
safeDivideは、クロージャが設定した名前付き戻り値errとともに、mainへ普通に戻った。
deferした関数がエラーを返せるのは、名前付き戻り値があるからです。なければ関数はゼロ値を返します。deferしたクロージャが戻り値を変更する仕組みはdeferのページで扱っています。
panicがないとき recover() は nil を返すので、if r != nil のチェックによって、deferした関数は通常の経路では何もしません。deferした関数の外や、deferした関数から呼ばれた関数の中で呼ぶと、recover は nil を返して何もしません。
独自の値でpanicする
panic はどんな値でも受け取ります。エラーか文字列が一般的です。
想定しているものだけを回復し、それ以外は再びpanicさせます。すべてのpanicを握りつぶすと、本物のバグが隠れます。
Go 1.21以降、panic(nil) は *runtime.PanicNilError に変換されるので、recover() が nil を返せば確実に「panicはない」という意味になりました。
ゴルーチンの中のpanic
recover が捕まえるのは、自分のゴルーチンのpanicだけです。recoverのないゴルーチンでpanicが起きると、main や他のすべてのゴルーチンを含め、プロセス全体が終了します。
2つのワーカーの行はどちらの順でも表示されえますが、main finished は常に最後です。main の中の defer recover() では、2つ目のワーカーからプログラムを救えなかったでしょう。HTTPサーバーがリクエストごとにrecoverするのはこのためです。net/http は各ハンドラのゴルーチンでpanicを回復してログに出し、その接続を閉じるので、1つの不正なリクエストでサーバーが落ちることはありません。
失敗の中にはpanicではなく致命的エラーで、まったく回復できないものもあります。concurrent map writes、メモリ不足、そしてデッドロック検出器の all goroutines are asleep です。
panicするのが正しい場面
Goの一般的なルールは、実行時に起こりうることにはすべてエラーを返し、panicはプログラマーの間違いのときだけにする、というものです。具体的には、panicが適切なのは次のようなときです。
- 不変条件が壊れた。 自分の列挙型に対する
switchが、起こりえないケースに到達した。続行するとデータが壊れます。 Mustヘルパーが不正な定数の入力を受け取った。regexp.MustCompile、template.Must、uuid.MustParseはエラーを返す関数を包み、失敗するとpanicします。コンパイル時にわかっている値、典型的にはパッケージレベルの変数に使います。そこでの失敗はソースコードが間違っていることを意味します。
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- 起動を続けられない。
mainで必須の設定がない場合です。ただ、ここでもスタックトレースより、エラーを表示してos.Exit(1)を呼ぶほうがすっきりすることが多いです。
panicが間違った道具なのは次のようなときです。
- 想定される失敗:不正なユーザー入力、存在しないファイル、タイムアウト。
errorを返します。エラー処理のページを参照してください。 - 制御の流れ:大きな呼び出しツリーをまたいでpanicとrecoverを例外のように使うと、コードを追いにくくなります。標準ライブラリは内部でいくつかの場所(
encoding/jsonのエンコーダー)でこれを使っていますが、常に返す前に回復しているので、パッケージの外にpanicが漏れることはありません。 - ライブラリのAPI:不正な入力でpanicするライブラリは、すべての呼び出し側にrecoverを加えることを強います。エラーを返します。
よくある間違い
- deferした関数の外でrecoverを呼ぶ。
nilを返します。 - ゴルーチンのpanicに対して
mainでrecoverする。 各ゴルーチンに専用のものが必要です。 - すべてのpanicを握りつぶす。 スタック(
runtime/debugのdebug.Stack())とともにログに出し、想定していなかったものは再びpanicさせます。 - nilマップへの書き込みや範囲外のインデックスを
recoverで扱う。 代わりにバグを直します。
よくある質問
Goのpanicとは何ですか?
現在のゴルーチンの通常の流れを止める実行時の失敗です。Goはスタック上の各関数のdeferした呼び出しを内側から外側へ順に実行し、何もrecoverしなければ、プログラムはpanicの値とスタックトレースを表示してステータス2で終了します。panicはバグ(インデックス範囲外、nilポインタの参照外し、nilマップへの書き込み)か、明示的な panic(v) の呼び出しから生じます。
Goでpanicから回復するには?
deferした関数の中で recover() を呼びます:defer func() { if r := recover(); r != nil { ... } }()。これは panic に渡された値を返して巻き戻しを止めるので、それをdeferした関数は呼び出し側に普通に戻ります。それ以外の場所で呼ぶと、recover はnilを返して何もしません。
別のゴルーチンのpanicからrecoverできますか?
できません。recover が止められるのは、それが実行されているゴルーチンのpanicだけです。開始したゴルーチンでpanicが起き、そのゴルーチンの中にrecoverがなければ、プログラム全体がクラッシュします。panicしうるゴルーチンには、それぞれ専用のdeferしたrecoverが必要です。
エラーを返す代わりにpanicを使うべきなのはいつですか?
想定される失敗ではなく、バグやありえない状態のときです。不正な入力、存在しないファイル、ネットワークのエラーはエラーです。panicが妥当なのは、不変条件が壊れたとき、常に正しいはずの定数を Must ヘルパーに渡すとき(regexp.MustCompile)、プログラムがそもそも起動できないときです。ライブラリは公開APIの外にpanicを漏らすべきではありません。