Go 言語の遅延実行 defer がピンと来なかったのでわかりやすくまとめた
具体例で段階的に理解を試みるアプローチ。
初期化、処理、後始末をする例
例として初期化、処理、後始末をするコードを考える。
package main import "fmt" func initialize(){ fmt.Println("初期化") } func work(){ fmt.Println("なんか処理する") } func terminate(){ fmt.Println("後始末") } func main() { initialize() work() terminate() }
実行結果は以下のとおり。
初期化 なんか処理する 後始末
処理の途中でエラーが起きたとする
ここで、処理の途中で何かエラーが起きたとしよう。コードでは panic() 関数を使って無理矢理発生させている。
package main import "fmt" func initialize(){ fmt.Println("初期化") } func work(){ fmt.Println("なんか処理する") panic("処理の途中でなんかエラー起きた") // ★ } func terminate(){ fmt.Println("後始末") } func main() { initialize() work() terminate() }
実行結果は以下のとおり。
初期化 なんか処理する panic: 処理の途中でなんかエラー起きた
エラーが起きた時に後処理が実行されてない。後処理は必ず実行するものだ。実行させたい。さてどうするか。
後処理を実行させるために defer を使う
ここで defer が登場する。
package main import "fmt" func initialize(){ fmt.Println("初期化") } func work(){ fmt.Println("なんか処理する") panic("処理の途中でなんかエラー起きた") } func terminate(){ fmt.Println("後始末") } func main() { initialize() defer terminate() // ★defer付けた&順番変えた work() // ★ }
実行結果は以下のようになる。
初期化 なんか処理する 後始末 panic: 処理の途中でなんかエラー起きた
エラーで落ちる前に後処理(後始末)が実行されている。defer を指定し、かつ実行順序を(エラーが発生する)work() の前に書いたおかげだ。
この defer、内部的には何してんの?
ただ、これだけだと defer について、まだピンと来ない。
もう一つ例を出す。
package main import "fmt" func initialize(){ fmt.Println("初期化") } func work(){ fmt.Println("なんか処理する") panic("処理の途中でなんかエラー起きた") } func terminate1(){ fmt.Println("後始末1") } func terminate2(){ fmt.Println("後始末2") } func terminate3(){ fmt.Println("後始末3") } func main() { initialize() defer terminate3() // ★ defer を使った呼び出しを複数定義してみた defer terminate2() // ★ defer terminate1() // ★ work() }
これを実行すると以下結果が出る。
初期化 なんか処理する 後始末1 後始末2 後始末3 panic: 処理の途中でなんかエラー起きた
defer の定義では 3 → 2 → 1 と書いているが、実際は 1 → 2 → 3 の順で実行されている。これはどういうことか。
結論を言うと スタック である。もっと言うと 「スコープから抜ける時に必ず実行するヤツら」スタック だ。
まず defer X
を行うと、内部的には X がスタックに入れられる。で、このスタックだが、(この defer を定義したスコープから抜ける時)に Go 言語がチェックして、中身を全部実行するようになっている。実行順はスタックだから LIFO、つまり後入れ先出しだ。
ここでもう一つ重要なのは エラーが起きた時もスコープを抜ける ということ。
func main(){ work1() work2() // ★ここでエラーが起きたら、ここで main() のスコープを抜ける work3() // work3 以降は実行されない work4() }
ここまで押さえれば defer の挙動についてイメージが湧くと思う。
おわりに
まとめ:
defer X は「スコープから抜ける時に必ず実行するスタック」に X を Push する処理である
TODO: Go言語の実装を見て真実を確かめる(いい線言ってるとは思う) or ご存知の方教えてください。。。