こんにちは、いっちゃんです。10ANTZでは、サーバーサイドのソフトウェア開発に携わっています。
本記事では、2023年9月4日に開催された社内勉強会の復習をします。10ANTZのエンジニアの間では不定期で勉強会が開催されており、業務と直接関係あるかどうかに関わらず幅広いテーマが取り上げられています。今回は、Goクイズと題しまして、プログラミング言語Goの振る舞いに関するクイズを出題しました。以下では、ここで出題したクイズの中で、deferの振る舞いに関するものについてピックアップして解説します。本記事は、すでに公開されている記事「Goクイズ(nil編)https://developers.10antz.co.jp/archives/3835 」の続きとなっています。
目次
クイズの形式
前回と同様に、クイズは、短いGoのプログラム(main.go)と4つの選択肢からなります。選択肢は以下の4つです。
- コンパイルエラー(go build -o main main.goが失敗)
- 実行時エラー(./main を実行した時,終了ステータスが0以外)
- Xを出力(./main を実行した時,標準出力にXが含まれる)
- Yを出力(./main を実行した時,標準出力にYが含まれる)
解答では、これらの選択肢の中から該当するものを全て選択します。
出題したクイズ
以下に勉強会で出題したクイズとその解答を示します。番号は前回の続きからとなっています。
クイズ4
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import ( "fmt" ) func f() { defer func() { fmt.Println("X") }() panic("fail") } func main() { f() fmt.Println("Y") } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
2、3
defer は return の直前に実行されます。panic すると関数は途中で return し、呼び出し元でさらに panic します。panic した時でも return する直前に defer が実行されます。
https://go.dev/ref/spec#DeferStmt
https://go.dev/blog/defer-panic-and-recover
クイズ5
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import ( "fmt" ) func f() (a string) { defer func() { a = "X" }() return "Y" } func main() { fmt.Println(f()) } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
3
ここで「return “Y”」は、返り値パラメータへの代入「a = “Y”」と返り値パラメータのリターン「return a」を合わせたに分解したものと同じ意味と考えることができます。分解した場合の return の直前に defer が実行されるため、返り値は“X”となります。
https://go.dev/ref/spec#DeferStmt
クイズ6
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import ( "fmt" ) func f() string { b := "X" defer func() { b = "Y" }() return b } func main() { fmt.Println(f()) } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
3
ここで「return b」は、暗黙の返り値パラメータxへの代入「x = b」と暗黙の返り値パラメータxのリターン「return x」に分解したものと同じ意味と考えることができます。分解した場合の return の直前に defer が実行されるため、返り値は“X”となります。
https://go.dev/ref/spec#DeferStmt
クイズ7
プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package main import ( "fmt" ) func main() { c := "Z" defer fmt.Println(c) c = "Y" defer fmt.Println(c) c = "X" } |
選択肢
- コンパイルエラー
- 実行時エラー
- Xを出力
- Yを出力
解答
4
defer で実行される関数では、引数はdefer呼び出しを通過した時点で評価されます。まず二つ目の defer により“Y”が出力され、次に一つ目の defer により“Z”が出力されます。
https://go.dev/ref/spec#DeferStmt
解説
これらのクイズは全て、Goにおける defer に関して私が分かりにくいと感じた振る舞いをもとに作成しました。これらの振る舞いについて分かりにくいと感じる人は他にもいると思います。実際、上のクイズについて、社内勉強会における正答率(回答者のうち、正答した人の割合)は、クイズ4から順に0.45、0.25、0.67、0.10となり、正答率の極端に低いクイズが存在します。
これらの振る舞いを理解するための考え方を、panicの連鎖、deferのタイミング、返り値のreturnに分けて解説します。
panicの連鎖
panicが発生すると、関数の実行を中断し呼び出し元へ制御が戻ります。さらに呼び出し元でもpanicが発生したように動作します。呼び出し元の関数がなくなるとプログラムは実行時エラーとなります。
deferのタイミング
deferをつけた関数の呼び出しにおいて、渡された引数はdefer呼び出しを通過した時点で評価され、関数の実行が処理されるまで値が保存されます。関数の実行は、その時点では処理されず、スタックされます。呼び出し元へ制御が戻る直前に、後にスタックされた関数の実行から順に処理されます。
返り値のreturn
返り値のreturnは、返り値を返り値パラメータへ代入して呼び出し元へ制御を戻すことと同じ意味となります。明示的な返り値パラメータがない場合でも、暗黙的な返り値パラメータがあると考えることができ、その場合呼び出し元へ制御が戻るタイミングは、返り値が暗黙的な返り値パラメータへ代入された直後となります。
例えば、以下に示すように、関数定義(1)は関数定義(2)と同じ意味となります。
1 2 3 4 |
// 関数定義(1) func f() string { return "abc" // 返り値のreturn } |
1 2 3 4 5 |
// 関数定義(2) func f() (s string) { // 暗黙的な返り値パラメータ s = "abc" // 返り値の返り値パラメータへの代入 return // 呼び出し元へ制御が戻る } |
例
以上の考え方を組み合わせて考えることで、例えば以下のようなプログラムが「abc」を出力する仕組みを理解できると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import "fmt" func f() (s string) { defer func() { fmt.Println(s) }() // => abc return "abc" // 1. "abc"が返り値パラメータsに代入される // 2. defer呼び出しされた関数の実行が処理されsの値が出力される // 3. 呼び出し元へ制御が戻る。 } func main() { f() } |
まとめ
本記事では、社内勉強会で出題したGoの振る舞いに関するクイズのうち、deferに関するものをピックアップして紹介し、解説しました。解説では、panicの連鎖、deferのタイミング、返り値のreturnについての考え方を紹介した上で、それらの組み合わせて考えることで、deferに関する分かりにくい振る舞いを理解できることを示しました。