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 myInt
のmyInt
はどのタイミングでインクリメントされているのか、気になりますね。
上記に追記して試してみましょう。
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
うーむ、これを見る限り、
- return文の式を評価する
- 遅延関数を実行する (返り値の書き換えがあれば書き換える)
- 返り値を呼び出し元に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が呼び出されると、
- panicが起きた時点でそれ以降に定義されている処理は行われず
- 呼び出し元(親)に
return
する - 親では
F()
部分がpanicの呼び出しのように振る舞う - 親の処理が止まる
- さらにその上の呼び出し元(親の親)に
return
する - 親の親では親関数がpanicの呼び出しのように振る舞う
- ...以下、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の機構は分かったけど、結局実用コードを書かないとね、感。