勉強過程で普通に panic になりますとかって言ってたけど panic とはなんぞや?ということで動きを見てみる。
端的に言うと panic は C# でいうところの例外に近いもの。Java でいうところの RuntimeException や、もっというと Error 系の例外クラスに近いもの。 つまり端的にいうと起きたら死亡という類のもの。
何個か発生させる方法としてぱっと思いつくのは範囲外アクセスや
package main import"fmt"func main() { s := []int{1, 2, 3} fmt.Println(s[10:]) // 範囲外!! }
タイプアサーション失敗や
package main import"fmt"func main() { var i interface{} = ""var j = i.(int) // string -> int はダメ fmt.Println(j) }
nil へのアクセスや
package main import"fmt"type Interface interface { Foo() } func main() { var i Interface i.Foo() // panic }
nil の値の参照とか
package main import"fmt"func main() { var s *string fmt.Println(*s) // panic!! }
あと自分で panic 関数を呼んでも起こせる。
package main func main() { panic("panic!!") }
今までは panic に甘んじて強制終了を受け入れてきたけど一応復帰は出来る。でも panic が起きるのは致命的なときだから、panic を前提にロジックを組むのはご法度。 復帰は recover 関数が呼ばれると、そこから復帰できる。でも panic が起きたら普通はコードは実行されずに落ちる。ということで defer でやらないとダメっぽい。
こんな感じ。
package main import"fmt"func myRecover() { r := recover() fmt.Printf("recovered: %v\n", r) } func myLogic() { defer myRecover() // リカバリーするのを仕込んで置いてpanic("panic!!") // panic を起こす } func main() { myLogic() fmt.Println("正常終了") }
実行するとこんな感じ。
recovered: panic!! 正常終了
panic に渡したものが recover を呼び出したときに返される。何もエラーないと nil なので正常ケースに備えて nil のチェックは必要。 試しに panic 呼び出しをコメントアウトすると nil と表示される。
package main import"fmt"func myRecover() { r := recover() fmt.Printf("recovered: %v\n", r) } func myLogic() { defer myRecover() // リカバリーするのを仕込んで置いて// panic("panic!!") // panic を起こす } func main() { myLogic() fmt.Println("正常終了") }
実行結果
recovered: <nil> 正常終了
panic の引数は interface{}
なので何でも渡せる。recover は func() interface{}
になってる。
ということはタイプアサーションを使えば特定のエラーのときだけ復帰することも可能。
例えば
package main import"fmt"func myRecover() { r := recover() if i, ok := r.(int); ok { // panic に渡されたものが int なら復帰 fmt.Printf("recovered: %v\n", i) } else { // そうじゃなければ panicpanic(r) } } func myLogic() { defer myRecover() panic(10) // int を渡す } func main() { myLogic() fmt.Println("正常終了") }
この場合は正常終了
recovered: 10 正常終了
panic に string を渡すと
package main import"fmt"func myRecover() { r := recover() if i, ok := r.(int); ok { // panic に渡されたものが int なら復帰 fmt.Printf("recovered: %v\n", i) } else { // そうじゃなければ panicpanic(r) } } func myLogic() { defer myRecover() panic("panic !!") // string を渡す } func main() { myLogic() fmt.Println("正常終了") }
panic になる。
panic: panic !! [recovered] panic: panic !! goroutine 1 [running]: main.myRecover() c:/Users/kaota/go/src/sample/main.go:12 +0xcf panic(0x4a1120, 0x4d6100) C:/Go/src/runtime/panic.go:513 +0x1c7 main.myLogic() c:/Users/kaota/go/src/sample/main.go:18 +0x5c main.main() c:/Users/kaota/go/src/sample/main.go:22 +0x29 exit status 2
ちゃんとスタックトレース(go でもそういうのかな?) が保持されて myLogic 内で panic が起きたこともわかるのでいい。 実行するまでは、これやるともしかしてスタックトレース消えちゃうのでは?と思ってたけど、そんなことなくて素敵。
まとめ
C 言語で goto を使うのが妥当だ!! と言われるような 凄く深い所からの一気に脱出したほうが望ましいケース 以外では panic / recover を前提としたロジックは組まないほうが良さそう。 これは、あくまで致命的なエラーを投げるためのものだ。