いろいろ備忘録日記

主に .NET とか Java とか Python絡みのメモを公開しています。最近Go言語勉強中。

Goメモ-18 (遅延呼び出し, Defer, Tour of Go)

概要

Tour of Go の - Defer についてのサンプル。

tour.golang.org

defer は、 Go言語の特徴的な機能の一つですね。 defer ステートメントは、deferに渡した関数の実行を呼び出し元の関数の終わりまで遅延させる機能です。

他の言語でいうと、関数レベルで設置した try-finally みたいな感じです。

func M() {
    defer fmt.Println("first")
    defer fmt.Println("second")
}

は、例えばC#だと

void M()
{
    try
    {
    }
    finally
    {
        Console.WriteLine("second");
        Console.WriteLine("first");
    }
}

のようなイメージ。

defer に指定するのは、関数の呼び出しです。自分で作った関数でもオッケイだし、匿名関数でもオッケイ。

defer func() {
    fmt.Println(100)
}()

匿名関数を使う場合は、最後に呼び出しの()が必要なことに注意です。

別に戻り値がある関数でも問題ありません。ただし、defer後にその戻り値は誰も受け取らないので闇に消えてしまいます。

defer は、Go言語でプログラムを記述する際に頻発する機能で、例えばI/O 処理や非同期処理などでよく利用します。

よくやるのが、以下のパターン。

f, err := os.Open("xxx")
if err != nil {
    return err
}

defer f.Close()

deferの呼び出しは、上記処理を内包している関数が終了するときに呼ばれます。

上記の場合、ほとんどのケースだと上でオッケイだと思います。が、実務でやるレベルで、クローズするときにファイルが存在しない可能性も考慮するのであれば

defer func() {
    closeErr := f.Close()
    if closeErr != nil {
        // ファイル閉じるときにエラーでた
    }
}()

の方が良いときもありますね。

deferの呼び出しは、内部でスタックされていて、関数内で最後に defer したものから順に着火していきます。(LIFO)

つまり、関数の最初に defer したものは、最後に呼び出されます。

注意点として

  • defer に指定した関数の呼び出し評価は遅延されるが、関数の引数に指定された引数の値は、遅延せずにその場で評価される
  • defer 内では、関数の return 変数 の値を読み書きできる

という特徴があるのですが、これはコードで見たほうが分かりやすいと思います。

サンプル

package tutorial

import "fmt"

// Defer は、 Tour of Go - Defer (https://tour.golang.org/flowcontrol/12) の サンプルです。
func Defer() error {
    // ------------------------------------------------------------
    // Go言語の defer について
    // defer は、 Go言語の特徴的な機能の一つ.
    // defer ステートメントは、deferに渡した関数の実行を呼び出し元の
    // 関数の終わりまで遅延させる機能。
    //
    // 他の言語でいうと、関数レベルで設置した try-finally みたいな感じ.
    // defer は、Go言語でプログラムを記述する際に頻発する機能で
    // 例えば、I/O 処理や非同期処理などでよく利用する
    //
    // defer には、匿名関数も指定することができる。
    // defer は、関数の呼び出しを要求するので匿名関数を利用する場合
    //   defer func(){}()
    // と記載する必要がある
    //
    // defer は、内部でスタックされており
    // 関数内で後で defer を呼び出した順から着火されていく. (LIFO)
    // つまり、関数内で最初に defer したものは、最後に実行される.
    //
    // 注意点として
    //   defer に指定した関数の呼び出し評価は遅延されるが
    //   関数の引数に指定された引数の値は、遅延せずにその場で評価される
    //
    //   defer 内では、関数の return 変数 の値を読み書きできる
    // というのがある.
    //
    // 参考:
    // https://golang.org/doc/effective_go.html#defer
    // https://blog.golang.org/defer-panic-and-recover
    // ------------------------------------------------------------
    defer func() {
        fmt.Println("defer - begin")
    }()

    func1()
    defer func2()
    func3()

    // defer に指定された Println(i) の i の値は
    // 遅延評価されず、すぐに評価されるので、0 が出力される
    i := 0
    defer fmt.Println(i)
    i++

    // defer 内で その関数の return変数 にアクセスできる
    fmt.Println("deferReadWriteReturnValue: ", deferReadWriteReturnValue())

    defer func() {
        fmt.Println("defer - end")
    }()

    return nil
}

func deferReadWriteReturnValue() (i int) {
    i = 0
    defer func() {
        // defer 内で return変数 を加算している
        // なので、この関数の本来の処理上で return した
        // 時点では、値は 1 だが、ここで更に加算されて
        // 結果として 2 が返る.
        i++
    }()

    i++

    return
}

func func3() {
    fmt.Println("func3()")
}

func func2() {
    fmt.Println("func2()")
}

func func1() {
    defer func() {
        fmt.Println("defer - func1")
    }()

    fmt.Println("func1()")
}

try-golang/tutorial_gotour_12_defer.go at master · devlights/try-golang · GitHub

実行すると以下な感じ。

[Name] "tutorial_gotour_defer"
func1()
defer - func1
func3()
deferReadWriteReturnValue:  2
defer - end
0
func2()
defer - begin

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com