recoverは同じgoroutine内のpanicしか捕まえない
recoverはそれを呼び出したgoroutine内で発生したpanicしか回復できません。別のgoroutine内でdeferとrecoverを仕込んでも、他のgoroutineのpanicは捕まえられずプログラム全体がクラッシュします。
エラーメッセージの読み方
panic: boom
panic- パニック — 実行時に回復不能な状態になったことを示します。deferとrecoverで捕まえない限り、そのままプロセスが終了します
boom- 詳細メッセージ — どの値が問題だったか
このエラーが出る典型パターン
パターン1
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func worker() { 9 panic("boom") 10 } 11 12 func main() { 13 defer func() { 14 if r := recover(); r != nil { 15 fmt.Println("recovered:", r) 16 } 17 }() 18 go worker() ^ 19 time.Sleep(100 * time.Millisecond) 20 }
別のgoroutineで発生したpanicは、mainのdeferにあるrecoverでは捕まえられません。recoverはpanicと同じgoroutine内でしか効果がありません。
直し方: go worker() を worker() にします。
パターン2
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func task() { 9 panic("fail") 10 } 11 12 func main() { 13 defer func() { 14 if r := recover(); r != nil { 15 fmt.Println("recovered:", r) 16 } 17 }() 18 go task() ^ 19 time.Sleep(100 * time.Millisecond) 20 }
goで起動したgoroutine内のpanicは、そのgoroutine自身がrecoverしない限り、プロセス全体をクラッシュさせます。
直し方: go task() を task() にします。
パターン3
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func job() { 9 panic("error") 10 } 11 12 func main() { 13 defer func() { 14 if r := recover(); r != nil { 15 fmt.Println("recovered:", r) 16 } 17 }() 18 go job() ^ 19 time.Sleep(100 * time.Millisecond) 20 }
main関数にrecoverを仕込んでおけばどのgoroutineのpanicも防げる、という思い込みは危険です。
直し方: go job() を job() にします。
パターン4
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func process() { 9 panic("crash") 10 } 11 12 func main() { 13 defer func() { 14 if r := recover(); r != nil { 15 fmt.Println("recovered:", r) 16 } 17 }() 18 go process() ^ 19 time.Sleep(100 * time.Millisecond) 20 }
安全にしたいなら、goroutineの中自身にdeferとrecoverのペアを用意する必要があります。
直し方: go process() を process() にします。
パターン5
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func handle() { 9 panic("oops") 10 } 11 12 func main() { 13 defer func() { 14 if r := recover(); r != nil { 15 fmt.Println("recovered:", r) 16 } 17 }() 18 go handle() ^ 19 time.Sleep(100 * time.Millisecond) 20 }
panicとrecoverの対応はgoroutineごとに独立しているため、goroutineをまたいだ回復はできません。
直し方: go handle() を handle() にします。
よくある誤解
「main関数でrecoverを仕込んでおけば、どのgoroutineのpanicも捕まえられるはず」という思い込みは誤りです。panicとrecoverはgoroutineごとに独立しており、panicしたgoroutine自身がdefer内でrecoverを呼ばない限り、そのpanicは回復できずプロセス全体を終了させます。
まとめ
recoverは同じgoroutine内のpanicしか捕まえないは上級でつまずきやすい項目です。上の5パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。