Menu

Go言語のpanicとrecover:実行時panicとpanicすべき場面

panicは通常の実行を止め、deferした呼び出しを実行しながらスタックを巻き戻します。panicの原因、deferした関数の中のrecoverでpanicを止める方法、目にすることになる実行時エラーのメッセージ、そしてpanicするのが正しい場面を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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 dereferencenilポインタを通したフィールドの読み取りや呼び出し
assignment to entry in nil map作成されていないマップへの書き込み
interface conversion: interface {} is int, not string間違った型への1値の型アサーション
integer divide by zero0による整数の除算や剰余(浮動小数点数なら代わりに +InfNaN
close of closed channelsend 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回目の呼び出しで起きたことは次のとおりです。

  1. a / b がpanicした。
  2. deferしたクロージャが実行され、recover() がpanicの値(runtime.Error)を返した。
  3. 巻き戻しが止まった。safeDivide は、クロージャが設定した名前付き戻り値 err とともに、main へ普通に戻った。

deferした関数がエラーを返せるのは、名前付き戻り値があるからです。なければ関数はゼロ値を返します。deferしたクロージャが戻り値を変更する仕組みはdeferのページで扱っています。

panicがないとき recover()nil を返すので、if r != nil のチェックによって、deferした関数は通常の経路では何もしません。deferした関数の外や、deferした関数から呼ばれた関数の中で呼ぶと、recovernil を返して何もしません。

独自の値で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.MustCompiletemplate.Mustuuid.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/debugdebug.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を漏らすべきではありません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める