Go言語の PanicとRecover の詳細な解説: 知っておくべきすべて!
Lukas Schneider
DevOps Engineer · Leapcell

Go言語の PanicとRecover の詳細な解説: 知っておくべきすべて!
Go言語には、panicとrecoverという、対になって現れることが多い2つのキーワードがあります。これら2つのキーワードはdeferと密接に関連しています。どちらもGo言語の組み込み関数であり、互いに補完的な機能を提供します。
I. panicとrecoverの基本的な機能
- panic: プログラムの制御フローを変更できます。panicを呼び出すと、現在の関数の残りのコードの実行が直ちに停止され、現在のGoroutineで呼び出し元のdeferが再帰的に実行されます。
- recover: panicによって引き起こされるプログラムのクラッシュを停止できます。deferでのみ有効になる関数です。他のスコープで呼び出しても効果はありません。
II. panicとrecoverを使用する際の現象
(I) panicは現在のGoroutineのdeferのみをトリガーする
次のコードは、この現象を示しています。
func main() { defer println("in main") go func() { defer println("in goroutine") panic("") }() time.Sleep(1 * time.Second) }
実行結果は次のとおりです。
$ go run main.go
in goroutine
panic:
...
このコードを実行すると、main関数のdeferステートメントは実行されず、現在のGoroutineのdeferのみが実行されることがわかります。これは、deferキーワードに対応するruntime.deferprocが、遅延呼び出し関数を呼び出し元が存在するGoroutineに関連付けるため、プログラムがクラッシュすると、現在のGoroutineの遅延呼び出し関数のみが呼び出されるためです。
(II) recoverはdeferで呼び出された場合にのみ有効になる
次のコードは、この機能を反映しています。
func main() { defer fmt.Println("in main") if err := recover(); err != nil { fmt.Println(err) } panic("unknown err") }
実行結果は次のとおりです。
$ go run main.go
in main
panic: unknown err
goroutine 1 [running]:
main.main()
...
exit status 2
このプロセスを注意深く分析すると、recoverはpanicが発生した後に呼び出された場合にのみ有効になることがわかります。ただし、上記の制御フローでは、recoverはpanicの前に呼び出されており、有効になるための条件を満たしていません。したがって、recoverキーワードはdeferで使用する必要があります。
(III) panicはdeferで複数のネストされた呼び出しを許可する
次のコードは、defer関数でpanicを複数回呼び出す方法を示しています。
func main() { defer fmt.Println("in main") defer func() { defer func() { panic("panic again and again") }() panic("panic again") }() panic("panic once") }
実行結果は次のとおりです。
$ go run main.go
in main
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
...
exit status 2
上記のプログラムの出力結果から、プログラムでpanicを複数回呼び出しても、defer関数の通常の実行には影響しないことがわかります。したがって、通常、最終処理にはdeferを使用するのが安全です。
III. panicのデータ構造
Go言語のソースコードのpanicキーワードは、データ構造runtime._panicで表現されます。panicが呼び出されるたびに、次の構造のようなデータ構造が作成され、関連情報が格納されます。
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool pc uintptr sp unsafe.Pointer goexit bool }
- argp: deferが呼び出されたときのパラメーターへのポインターです。
- arg: panicが呼び出されたときに渡されるパラメーターです。
- link: 以前に呼び出されたruntime._panic構造体を指します。
- recovered: 現在のruntime._panicがrecoverによって回復されたかどうかを示します。
- aborted: 現在のpanicが強制的に終了されたかどうかを示します。
データ構造のlinkフィールドから、panic関数は連続して複数回呼び出すことができ、linkを介してリンクリストを形成できると推測できます。
構造体の3つのフィールドpc、sp、およびgoexitはすべて、runtime.Goexitによってもたらされる問題を修正するために導入されました。runtime.Goexitは、他のGoroutineに影響を与えることなく、この関数を呼び出すGoroutineのみを終了できます。ただし、この関数はdeferのpanicとrecoverによってキャンセルされます。これらの3つのフィールドの導入は、この関数が確実に有効になるようにするためです。
IV. プログラムクラッシュの原理
コンパイラーは、キーワードpanicをruntime.gopanicに変換します。この関数の実行プロセスには、次の手順が含まれます。
- 新しいruntime._panicを作成し、それが存在するGoroutineの_panicリンクリストの先頭に追加します。
- 現在のGoroutineの_deferリンクリストからruntime._deferをループで継続的に取得し、runtime.reflectcallを呼び出して遅延呼び出し関数を実行します。
- runtime.fatalpanicを呼び出して、プログラム全体を中断します。
func gopanic(e interface{}) { gp := getg() ... var p _panic p.arg = e p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) for { d := gp._defer if d == nil { break } d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) if p.recovered { ... } } fatalpanic(gp._panic) *(*int)(nil) = 0 }
上記の関数では、コードの3つの比較的重要な部分が省略されていることに注意してください。
- プログラムを復元するためのrecoverブランチのコード。
- インライン化によるdefer呼び出しのパフォーマンスを最適化するためのコード。
- runtime.Goexitの異常な状況を修正するためのコード。
バージョン1.14では、Go言語は、「runtime: 再帰的なpanic/recoverによってGoexitを中断できないようにする」のコミットを通じて、再帰的なpanicとrecoverとruntime.Goexitの間の競合を解決しました。
runtime.fatalpanicは、回復できないプログラムクラッシュを実装します。プログラムを中断する前に、runtime.printpanicsを介してすべてのpanicメッセージと呼び出し中に渡されたパラメーターを出力します。
func fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() if startpanic_m() && msgs != nil { atomic.Xadd(&runningPanicDefers, -1) printpanics(msgs) } if dopanic_m(gp, pc, sp) { crash() } exit(2) }
クラッシュメッセージを出力した後、runtime.exitを呼び出して現在のプログラムを終了し、エラーコード2を返します。プログラムの通常の終了もruntime.exitを介して実装されます。
V. クラッシュ回復の原理
コンパイラーは、キーワードrecoverをruntime.gorecoverに変換します。
func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil &&!p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
この関数の実装は非常に簡単です。現在のGoroutineがpanicを呼び出していない場合、この関数は直接nilを返します。これは、defer以外で呼び出すとクラッシュ回復が失敗する理由でもあります。通常の状態では、runtime._panicのrecoveredフィールドを変更し、プログラムの回復はruntime.gopanic関数によって処理されます。
func gopanic(e interface{}) { ... for { // 遅延呼び出し関数を実行します。これにより、p.recovered = trueが設定される場合があります ... pc := d.pc sp := unsafe.Pointer(d.sp) ... if p.recovered { gp._panic = p.link for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { gp.sig = 0 } gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") } } ... }
上記のコードは、deferのインライン最適化を省略しています。runtime._deferからプログラムカウンターpcとスタックポインターspを取り出し、runtime.recovery関数を呼び出してGoroutineのスケジューリングをトリガーします。スケジューリングの前に、sp、pc、および関数の戻り値を準備します。
func recovery(gp *g) { sp := gp.sigcode0 pc := gp.sigcode1 gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 gogo(&gp.sched) }
deferキーワードが呼び出されると、呼び出し時のスタックポインターspとプログラムカウンターpcはすでにruntime._defer構造体に格納されています。ここでのruntime.gogo関数は、deferキーワードが呼び出された位置にジャンプバックします。
runtime.recoveryは、スケジューリングプロセス中に関数の戻り値を1に設定します。runtime.deferprocのコメントから、runtime.deferproc関数の戻り値が1の場合、コンパイラーによって生成されたコードは、呼び出し元関数の戻り値の前に直接ジャンプし、runtime.deferreturnを実行することがわかります。
func deferproc(siz int32, fn *funcval) { ... return0() }
runtime.deferreturn関数にジャンプした後、プログラムはpanicから回復し、通常のロジックを実行します。また、runtime.gorecover関数は、panicの呼び出し時に渡されたargパラメーターをruntime._panic構造体から取り出して、呼び出し元に返すことができます。
VI. まとめ
プログラムのクラッシュと回復のプロセスを分析するのは非常にトリッキーであり、コードも特に理解しやすいわけではありません。プログラムのクラッシュと回復のプロセスを簡単にまとめます。
- コンパイラーは、キーワードを変換する作業を担当します。 panicとrecoverをそれぞれruntime.gopanicとruntime.gorecoverに変換し、deferをruntime.deferproc関数に変換し、deferを呼び出す関数の最後にruntime.deferreturn関数を呼び出します。
- 実行プロセス中にruntime.gopanicメソッドが発生すると、Goroutineのリンクリストからruntime._defer構造体を次々と取り出して実行します。
- 遅延実行関数を呼び出すときにruntime.gorecoverが発生した場合、_panic.recoveredをtrueとしてマークし、panicのパラメーターを返します。
- この呼び出しが終了した後、runtime.gopanicはruntime._defer構造体からプログラムカウンターpcとスタックポインターspを取り出し、runtime.recovery関数を呼び出してプログラムを復元します。
- runtime.recoveryは、渡されたpcとspに従ってruntime.deferprocにジャンプバックします。
- コンパイラーによって自動的に生成されたコードは、runtime.deferprocの戻り値が0ではないことを検出します。この時点で、runtime.deferreturnにジャンプバックし、通常の実行フローに戻ります。
- runtime.gorecoverが発生しない場合、すべてのruntime._deferを順番にトラバースし、最後にruntime.fatalpanicを呼び出してプログラムを中断し、panicのパラメーターを出力して、エラーコード2を返します。
分析プロセスには、言語の基盤レベルでの多くの知識が含まれており、ソースコードも比較的読みにくいものです。慣習にとらわれない制御フローで満たされており、プログラムカウンターを介して行ったり来たりします。ただし、プログラムの実行フローを理解するには、非常に役立ちます。
Leapcell:Golangホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォーム
最後に、最適なデプロイメントプラットフォームをご紹介します。Leapcell
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量のみを支払い—リクエストも料金もありません。
3. 比類のないコスト効率
- アイドル料金なしの従量課金。
- 例:$25で平均応答時間60msで694万リクエストをサポート。
4. 合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムのメトリックとロギング。
5. 簡単なスケーラビリティと高いパフォーマンス
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ—構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ