😽

[Go Quiz] 解説: Defer quiz

2022/09/22に公開

この記事は、Twitterで出題した次のGoクイズの解説です。

https://twitter.com/shino_nobishii/status/1572525163719303170

Quiz(再掲)

次のプログラムを実行した結果として適切なものを選んでください:

  1. HELLOと表示されて正常終了する
  2. 何も表示されずにpanicする
  3. HELLOと表示されてpanicする
  4. コンパイルエラーになる
func main() {
    var f func()
    defer f()
    println("HELLO")
    f = func() { recover() }
}

このクイズを作ったきっかけ

このクイズを作ったきっかけはyappliさまのTech Blogでした。
とてもわかりやすい記事をありがとうございました。

https://tech.yappli.io/entry/understanding-defer-in-go

解説

https://go.dev/ref/spec に基づいて一行ずつ見ていきます。根拠となる箇所は逐一引用していくので興味のある方は検索をかけてみてください。

func main() {
    var f func()
    defer f()
    println("HELLO")
    f = func() { recover() }
}

変数宣言 var f func()

https://go.dev/ref/spec#Variable_declarations

まず変数宣言var f func()を見ます。型func()を持つ変数fの宣言ですが、右辺の式が特定されていません。

そのようなときには、fは型func()ゼロ値で初期化されます。

If a list of expressions is given, the variables are initialized with the expressions following the rules for assignment statements. Otherwise, each variable is initialized to its zero value.

func()のゼロ値はnilですから、これはvar f func() = nilとしたのと同じことになります。

Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps.

Defer文defer f()

問題のdefer文です。

https://go.dev/ref/spec#Defer_statements

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.

拙訳: defer文が実行されるごとに、その関数値とその呼出しに対するパラメータは通常通りに評価され、改めて保存されますが、その実際の関数は実行されません。deferされた関数はそれを囲む関数がreturnする直前に、deferされたのと逆順で実行されます。

つまり、

  • 関数値とパラメータはdefer文の実行時に評価される
  • しかしその関数が実行されるのはreturnの直前

ことがわかります。defer文の時点では関数が実行されないのですから、少なくともこの時点でpanicすることはありません。

println("HELLO")

これは普通の文で、"HELLO"を出力します。

defer文の時点ではpanicしないので、問題のプログラムは少なくとも"HELLO"を出力します。

代入文f = func() { recover() }

ここで変数にffunc() { recover() }という関数を代入しています。

deferされた関数の実行

ここで関数のボディが終わりなので、defer文で指定した関数が実行されます:

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

この時点では変数ffunc() { recover() }に書き換わっています。しかし、deferされた関数の値はdefer文の実行時、つまりこのプログラムの2行目の時点で評価されたものが使われます。つまりdefer実行されるのはあくまでもnilです。

そしてnilである関数の呼び出しはpanicを引き起こします。

https://go.dev/ref/spec#Calls

Calling a nil function value causes a run-time panic.

サマリー

  • 変数宣言var f func()fnilで初期化される
  • その直後のdefer f()の時点ではfは実行されないが、fの値はこの時点での値であるnilが「保存」される。
    • 引数を渡した場合も同様
  • その後でfを書き換えても、defer実行されるfnilのままなので、panicを引き起こす

よって正解は3.の「HELLOと表示されてpanicする」です。

GitHubで編集を提案

Discussion