Defer、Panic、Recover
Defer, Panic, and Recover by Andrew Gerrand
Goには制御フローのための一般的な仕組みがあります。if、for、switch、gotoです。 また、別のゴルーチンでコードを実行するためのgo文もあります。ここでは、あまり一般的ではない defer、panic、recoverについて説明します。
defer文は関数呼び出しをリストに積みます。積まれた呼び出しのリストは、それを囲む関数がreturnした あとに実行されます。deferは、様々な後片付け処理を行う関数を単純化するためによく使われます。
例として、2つのファイルを開き、一方の内容をもう一方にコピーする関数を見てみましょう。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
これで動作はしますが、バグがあります。os.Createの呼び出しが失敗した場合、この関数は元のファイルを 閉じずにreturnしてしまいます。これは2つめのreturn文の前にsrc.Closeの呼び出しを入れれば簡単に 直せますが、もし関数がもっと複雑であれば、この問題はそう簡単には気付かれず、解決もされないかも しれません。defer文を導入することで、ファイルが必ず閉じられるようにできます。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
defer文によって、ファイルを開いた直後にそれを閉じることを考えられるようになり、関数内にreturn文が いくつあっても、ファイルが 必ず 閉じられることが保証されます。
defer文の振る舞いは単純明快で予測可能です。3つの単純なルールがあります。
- deferされる関数の引数は、defer文が評価されたときに評価されます。
この例では、「i」という式はPrintlnの呼び出しがdeferされるときに評価されます。deferされた呼び出しは、 関数がreturnしたあとに「0」を出力します。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
- deferされた関数呼び出しは、それを囲む関数がreturnしたあとに、後入れ先出し(LIFO)の順序で実行され ます。
この関数は「3210」を出力します。
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
- deferされた関数は、それを囲む関数の名前付き戻り値を読み書きできます。
この例では、deferされた関数が、それを囲む関数がreturnした あとに 戻り値iをインクリメントします。 したがって、この関数は2を返します。
func c() (i int) {
defer func() { i++ }()
return 1
}
これは関数のエラーの戻り値を変更するのに便利です。これについては後ほど例を見ます。
panicは制御の通常の流れを止め、_パニック_を開始する組み込み関数です。関数Fがpanicを呼び出すと、 Fの実行は停止し、F内でdeferされている関数が通常通り実行され、その後Fは呼び出し元にreturnします。 呼び出し元から見ると、Fの呼び出しはpanicの呼び出しのように振る舞います。この過程は、現在の ゴルーチン内のすべての関数がreturnするまでスタックを遡りながら継続し、その時点でプログラムは クラッシュします。panicはpanicを直接呼び出すことで開始できます。また、配列の範囲外アクセスのような ランタイムエラーによっても引き起こされます。
recoverはパニックを起こしているゴルーチンの制御を取り戻す組み込み関数です。recoverはdeferされた 関数の中でのみ有効です。通常の実行では、recoverの呼び出しはnilを返すだけで、それ以外の効果は ありません。現在のゴルーチンがパニックを起こしている場合、recoverの呼び出しはpanicに渡された値を 捕捉し、通常の実行を再開します。
panicとdeferの仕組みを示すサンプルプログラムを見てみましょう。
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
関数gは整数iを受け取り、iが3より大きければpanicを起こし、そうでなければ引数i+1で自分自身を呼び出し ます。関数fは、recoverを呼び出し、回復した値を(nilでなければ)出力する関数をdeferします。この先を 読む前に、このプログラムの出力がどうなるか想像してみてください。
このプログラムは次のように出力します。
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
fからdeferされた関数を取り除くと、panicは回復されずにゴルーチンの呼び出しスタックの最上位まで 到達し、プログラムを終了させます。この変更後のプログラムは次のように出力します。
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
panicとrecoverの実例としては、Go標準ライブラリのjsonパッケージを 見てみてください。jsonパッケージは、一連の再帰関数を使ってインターフェースをエンコードします。値を たどっている最中にエラーが発生すると、panicが呼び出されてスタックを最上位の関数呼び出しまで 巻き戻し、そこでpanicから回復して適切なエラー値を返します(encode.go 内のencodeState型の「error」メソッドと「marshal」メソッドを参照してください)。
Goのライブラリにおける慣習は、パッケージが内部でpanicを使っている場合でも、外部向けのAPIは 明示的なエラーの戻り値を提示する、というものです。
deferは、先に挙げたfile.Closeの例以外にも様々な後片付けに使えます。たとえば、次のようにmutexを 解放したり、
mu.Lock()
defer mu.Unlock()
次のようにフッターを出力したりします。
printHeader()
defer printFooter()
使い道は他にもたくさんあります。
まとめると、defer文は(panicとrecoverを伴う場合も伴わない場合も)制御フローのための珍しくて 強力な仕組みを提供します。他のプログラミング言語で特別な構造によって実装されている多くの機能を モデル化するために使えます。ぜひ試してみてください。
By Andrew Gerrand