読者です 読者をやめる 読者になる 読者になる

CUBE SUGAR CONTAINER

技術系のこと書きます。

Golang の defer 文と panic/recover 機構について

Golang の defer 文と、それにまつわる panic/recover 機構について調べたので、その内容を書き残しておくことにする。

Golang では defer 文を使うことで、それを呼び出した関数が終了する際に実行すべき処理を記述することができる。 例えば関数の中でオープンしたリソースを確実にクローズするために使われたりする。

試してみよう

まずは、シンプルなサンプルコードを使って defer 文の動作を確認してみることにする。 内容的にはメッセージを標準出力に出すということしかやっていない。 ただし、片方には defer が付いていて、もう片方には何もついていない。 コードの記述順序的には "End" のメッセージが先に来ているが、実行するとどうなるだろうか。

package main

import (
    "fmt"
)

func helloworld() {
    defer fmt.Println("End")
    fmt.Println("Start")
}


func main() {
    helloworld()
}

上記のコードを実行してみると、"Start" の方が先に出力されている。 これは、"End" の方には defer 文が付いたことで helloworld() 関数が終了する際に呼び出されるようになったため。

$ go run defer.go
Start
End

panic() したときにも呼び出される

前述したように、defer はリソースの後始末をするイディオムとして多用される。 であれば、関数の中で panic() 関数を呼び出した場合にもちゃんと処理されるようになっていなければ困る。 もし処理されない場合にはリソースに開放漏れが起こる可能性がある。

以下のサンプルコードでは、先ほどの内容に panic() 関数を一行追加している。

package main

import (
    "fmt"
)

func helloworld() {
    defer fmt.Println("End")
    fmt.Println("Start")
    panic("Panic!")
}


func main() {
    helloworld()
}

上記を実行すると、panic() 関数が呼ばれてもちゃんと "End" が出力されていることがわかる。 defer 文は panic() 関数が呼ばれるか否かに関わらず処理されることがわかった。

$ go run defer.go
Start
End
panic: Panic!

goroutine 1 [running]:
main.helloworld()
    /Users/amedama/Documents/temporary/go-defer/defer.go:10 +0x22a
main.main()
    /Users/amedama/Documents/temporary/go-defer/defer.go:15 +0x14
exit status 2

defer を使って panic() から recover() する

これもまたエラーハンドリングで多用されるイディオムだけど、defer 文を使って panic() したときの後始末が記述できる。

あらかじめ関数の呼び出しで panic() 関数が起こる可能性が予見できる場合には、以下のように defer 文を使ってその復旧方法を書いておく。 もし panic() 関数が呼ばれた場合には recover() 関数を使ってその内容を取得できる。 これは例外機構を備えた言語でいうところの try ~ catch 文に近いものがあるかな。

package main

import (
    "fmt"
)

func helloworld() {
    defer func() {
        fmt.Println("End")
        err := recover()
        if err != nil {
            fmt.Println("Recover!:", err)
        }
    }()

    fmt.Println("Start")
    panic("Panic!")
}


func main() {
    helloworld()
}

上記を実行すると panic() した内容が recover() によって取得できていることがわかる。

$ go run defer.go
Start
End
Recover!: Panic!

error vs panic/recover

さて、ここで疑問が生じる。 panic/recover の機構が他言語での例外機構のように振る舞うのであれば、error との使い分けをどうすれば良いのだろうか。 ここで補足しておくと、Golang の慣例ではエラーが発生する可能性のある関数に関しては、返り値を多値 (Python のタプルみたいなもの) で返して、その中のひとつが error インターフェースというオブジェクトになる

調べた限りでは、panic/recover について公式に語られているドキュメントとして以下を見つけることができた。

PanicAndRecover · golang/go Wiki · GitHub

Defer, Panic, and Recover - The Go Blog

Frequently Asked Questions (FAQ) - The Go Programming Language

その中に書かれている内容をざっくりまとめると次の通り。 尚、これは Golang の標準ライブラリで使用されている慣例に沿っているようだ。

  • 外部に公開する API はエラーを伝える手段として panic() を使ってはいけない
    • 多値の返り値 w/ error インターフェースを使うこと
    • panic() はパッケージをまたいで伝搬させることがないようにする
  • panic/recover 機構はスタックが深くなるような呼び出しをする際に有用
    • 使用することで可読性を向上させることができる

つまり、外部にエラーを伝える手段としては error インターフェースを使うのが正式なやり方で、panic/recover はあくまで内部的に用いるべきもの、ということらしい。