Defer
関数呼び出しをリストに Push する。周囲の関数を実行後、リストに貯まった関数が実行される。
Defer は、様々な Clean up 処理に使用される。
サンプルコード
ファイルをコピーする下記のコードを例にする。
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(dstName)
が失敗すると、コピー元のファイルを close することができない。
このバグは、以下のように src
に対して明示的に Close()
を呼び出すことで解決できる。
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
によって、各ファイルをオープン後に close することが保証される。
3つのルール
1. Defer
関数の引数は、Defer ステートメントが評価される時に評価される
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
func main() {
b()
}
$ go run main.go
0
上記を実行すると、0が出力される。i
は、Println()
が call された際に評価されることがわかる。
2. Defer
関数の呼び出しは、 LIFO(Last In First Out) にて実行される
func b() {
for i := 0; i < 4; i++ {
defer fmt.Println(i)
}
}
func main() {
b()
}
$ go run main.go
3
2
1
0
3. Defer
関数は、関数の戻り値を読み取り、新しい値を割り当てることができる
func c() (i int) {
defer func() { i++ }()
return 1
}
func main() {
fmt.Println(c())
}
$ go run main.go
2
上記の例では、c()
の返り値 i
に1を足す Defer
関数を作成している。よって、2が返ってくる。
3のルールは、エラー時の挙動を決定するにあたり、非常に有用である。
Panic
Panic() は、通常の制御の流れを止めて Panic を開始する組み込み関数である。関数FがPanicを呼び出すと、Fの実行が停止し、F内のすべての Defer
関数が正常に実行されてから、Fは呼び出し元に戻る。 Caller にとって、Fはパニックへの呼び出しのように動作する。プロセスは、現在の goroutine (Goのランタイムによって管理される軽量スレッド)内のすべての関数が戻るまで Stack を上っていく。戻ると、プログラムがクラッシュする。
Panic は、 Panic を直接呼び出すことによって開始できる。また、範囲外の配列アクセスなどのランタイムエラーが原因で発生することもある。
Recover
Recover は、 Panic 状態の goroutine の制御を取り戻す組み込み関数である。 Recover は、 Defer
関数内でのみ使用可能。通常の実行中、 Recover の呼び出しは nil を返し、他の効果はない。現在の goroutine が Panic になっている場合、Recoverの呼び出しは Panic に与えられた値をキャプチャし、通常の実行を再開する。
サンプルコード
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 nomally 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)
}
$ go run main.go
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.
Recover()
がなかった場合、以下のように goroutine のスタック最上部まで戻り、プログラムを終了させる。
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 nomally 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)
}
$ go run main.go
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
(StackTrace は省略。。。)
Panic と Recover の実例として、Go標準ライブラリのjsonパッケージを参照すると良い。jsonパッケージは、一連の再帰関数を使用してインターフェースをエンコードする。値のトラバース中にエラーが発生した場合、 Panic()
が呼び出されてスタックが最上位の関数呼び出しに巻き戻され、 Panic から回復して適切なエラー値が返される(encode.go)。
Goライブラリの規則では、パッケージが内部で Panic を使用している場合でも、その外部 API は明示的なエラー戻り値を表示する、としている。
他に、 Defer
の使用例として、 Mutex のリリースや、フッターの出力などがある。
mu.Lock()
defer mu.Unlock()
printHeader()
defer printFooter()