LoginSignup
174
72

More than 3 years have passed since last update.

Go言語のdeferを正しく理解する | How defer in Golang works

Posted at

deferってなんかかっこいい!
くらいの認識の人向け。

環境

go version go1.13.7 darwin/amd64

A Tour of Goより

A defer statement defers the execution of a function until the surrounding function returns.

The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.

defer文は、上位ブロックの関数がreturnするまで関数の実行を遅延させる。
遅延実行される関数の引数は即時評価されるが、関数の実行は上位ブロックの関数がreturnするまで実行されない。

出典: A Tour of Go

試してみる

なにはともあれコードを書く。

遅延関数の引数の即時評価について

まずは上記Tour of Goのリンク先にあるplaygroundで試してみましょう。

デフォルトだと下記のコード。

package main

import "fmt"

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}

実行すると

hello
world

お、遅延してますね。
試しに"world"部分を変数にして、その変数を書き換えてみましょうか。

package main

import "fmt"

func main() {
    world := "world"
    world = "world?"

    defer fmt.Println(world)

    fmt.Println("hello")
}

実行すると

hello
world?

書き換わっていますね。

ではworldの中身をdefer宣言の後に書き換えてみましょう。

package main

import "fmt"

func main() {
    world := "world"

    defer fmt.Println(world)

    world = "world?"

    fmt.Println("hello")
}

実行すると

hello
world

これが遅延実行される関数の引数は即時評価されるということですね。
遅延実行を定義した後にその引数をいくら書き換えても意味がないようです。

このおかげで意図せぬdeferの挙動を避けられそうですね。

deferの積み上げ

ではdeferを連続して宣言するとどうなるでしょうか。

こちらのplaygroundで試してみましょう。

package main

import "fmt"

func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    defer fmt.Println("last defer") // ここ追記しました

    fmt.Println("done")
}

元々はfor文内のdeferのみだったので、一応外側にもdefer追記してみました。
実行してみます。

counting
done
last defer
9
8
7
6
5
4
3
2
1
0

まあ特にfor文など関係なく、deferが処理されたのと逆順に実行されていくようです。
後入先出法で実行されるんですね。

返り値の書き換え

deferはreturnするまで実行されないというのはちょっと曖昧です。

ちょっとテストしてみましょう。
これに合致するTourは無いので、こちらででも実行してみてください。

package main

import (
    "fmt"
)

func test1() (myInt int) {
    myInt = 1

    defer func() {
        myInt++
    }()

    return myInt
}

func main() {
    fmt.Printf("test1: %d\n", test1())
}

これを実行すると

test1: 2

!?
キモいですね!?

まあ、遅延実行される関数は、上位関数の名前付き返り値を参照したり変更したりできるということが分かりました。

こうなるとreturn myIntmyIntはどのタイミングでインクリメントされているのか、気になりますね。

上記に追記して試してみましょう。

package main

import (
    "fmt"
)

func test1() (myInt int) {
    myInt = 1

    defer func() {
        myInt++
    }()

    return myInt
}

// 追記
func test2() (myInt int) {
    myInt = 1

    defer func() {
        myInt++
    }()

    return func() int {
        fmt.Println(myInt)
        return myInt
    }() 
}

func main() {
    fmt.Printf("test1: %d\n", test1())
    fmt.Printf("test2: %d\n", test2()) // 追記
}

実行すると。。。

test1: 2
1 # test2関数のreturn文で定義している無名関数内のPrintで表示したmyInt
test2: 2 # 実際に返ってきたmyInt

うーむ、これを見る限り、

  1. return文の式を評価する
  2. 遅延関数を実行する (返り値の書き換えがあれば書き換える)
  3. 返り値を呼び出し元にreturnする

というふうになっているようですね。

いずれにしてもこの特徴があることにより、エラー値の書き換えなどが便利になるようです。

panic, defer, recover

別にdeferの用途がこれだけというわけでは無いのですが、panicに対処する方法としてdeferを使う、というテンプレみたいなものがあります。

これはもはやGo公式のブログそのままなのですが、

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 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)
}

出典: The Go Blog | Defer, Panic, and Recover

main関数はfを呼び出し、fはgを呼び出します。
gは再帰的な関数になっており、受け取った引数をPrintして、引数をインクリメントした値でまた自身を呼び出します。

ただしgは引数が4以上の場合panicします。


panic

ここでpanicについても理解しておかないと理解できません。
ある関数Fの中でpanicが呼び出されると、

  1. panicが起きた時点でそれ以降に定義されている処理は行われず
  2. 呼び出し元(親)にreturnする
  3. 親ではF()部分がpanicの呼び出しのように振る舞う
  4. 親の処理が止まる
  5. さらにその上の呼び出し元(親の親)にreturnする
  6. 親の親では親関数がpanicの呼び出しのように振る舞う
  7. ...以下、goroutine内の全ての関数がreturnされるまでコールスタックをさかのぼり続け、最終的にプログラムがクラッシュする

という挙動になります。


さて、元のコードの話に戻ると、gがpanicを起こすと呼び出し元のfにreturnします。

で、fでもpanicが引き起こされ、特に何も対策をしていなければ、そのままプログラムがクラッシュするのが分かりますね。

そこで、「panicしたら呼び出し元にreturnする」というのがミソです。
deferはreturnする前にその処理が挟まることになるので、panicで他の処理が殺されても、deferで宣言した処理だけは生き残ります。

その中でrecover組み込み関数を使ってやると、panicの流れを食い止めることができます。

recover関数は、平常時に使用してもnilを返すのみですが、panic中に使用すると、panic()に渡された引数が返ってきます。
なのでr != nilの判定が入っているんですね。

recoverした後の処理についてはプログラマ次第です。
死ぬ前にやりたいことをやって再度panicさせるもよし、errを返り値として呼び出し元に返すもよし、エラーをどこかに記録して何事も無かったように処理を進めるもよし。

このケースではPrintしてpanicを握りつぶしてますね笑
実行結果はこのようになります。

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. # fがgで起きたpanicをrecoverしたので、呼び出し元のmain()は正常に処理を進められている

TL;DR

deferの機構は分かったけど、結局実用コードを書かないとね、感。

174
72
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
174
72