Go の defer を試した

最近 A Tour of Go を進めていて,defer というステートメントを知った.The Go Blog に詳細な解説記事が載ってたので,読みながら動かしてみた結果を簡単にまとめておく.

defer とは

関数の終了時に遅延実行する機能のこと.直感的に finally 的なアレかと思ったけど,実際に使ってみると全然違った.遅延実行されるタイミングは return だけじゃなくて panic もそう.さらに複数の処理をスタックできて LIFO で返ってくる点も興味深かった.

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

defer の特徴

記事にあるサンプルコードを少し修正して試してみた.

  1. A deferred function's arguments are evaluated when the defer statement is evaluated.
  2. Deferred function calls are executed in Last In First Out order after the surrounding function returns.
  3. Deferred functions may read and assign to the returning function's named return values.

上記の通りだけど,

  1. defer を評価したタイミングの値が保持される
  2. スタックすることができて LIFO で返ってくる
  3. 関数の戻り値(変数)にも適用できる

という感じ.

package main

import (
    "fmt"
)

func main() {
    // === Start sample1() ===
    // 3
    // 0
    // === Start sample2() ===
    // 0
    // 1
    // 2
    // 3
    // 4
    // Defer :  4
    // Defer :  3
    // Defer :  2
    // Defer :  1
    // Defer :  0
    // === Start sample3() ===
    // 4
    sample1()
    sample2()
    i := sample3()
    fmt.Println(i)
}

func sample1() {
    // `defer` は評価時の値を保持するため `0` のまま出力される
    fmt.Println("=== Start sample1() ===")
    i := 0
    defer fmt.Println(i)
    i++
    i++
    i++
    fmt.Println(i)
    return
}

func sample2() {
    // `defer` は複数をスタックすることができ、LIFO で返ってくる
    fmt.Println("=== Start sample2() ===")
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        defer fmt.Println("Defer : ", i)
    }
}

func sample3() (i int) {
    // `defer` で返り値 i に対してインクリメントが行われるため、`4` が返る
    fmt.Println("=== Start sample3() ===")
    defer func() {
        i++
        i++
        i++
    }()
    return 1
}

defer / panic / recover

記事にあるサンプルコードを写経した.defer をエラーハンドリングの panicrecover と組み合わせた例で参考になった.

重要なのは panic 時にも defer が呼び出されるところで,g()panic が呼ばれると g() のスコープ内で定義された defer が呼び出される.そして f() の中で deferrecover を定義してるので,エラーハンドリングできている.記事にある通り f()defer を消すと,その時点で panic となり main() には戻れなくなる.

package main

import (
    "fmt"
)

func main() {
    // 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 form f.
    f()
    fmt.Println("Returned normally form 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)
}

まとめ

記事に書かれてる以下の表現が凄く良いなと思った.

パワフルな魔法を使うのではなく,直感的で予測可能な記述を追求するのが Go のスタイルなんだなーと!

The behavior of defer statements is straightforward and predictable.