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 ご存知の方教えてください。。。