かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Go 言語勉強ログ その4 panic/recover

blog.okazuki.jp

勉強過程で普通に 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 {
        // そうじゃなければ panic
        panic(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 {
        // そうじゃなければ panic
        panic(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 を前提としたロジックは組まないほうが良さそう。 これは、あくまで致命的なエラーを投げるためのものだ。