はじめに
- Goの
defer
について「呼び出し元の関数の終了時に実行されるんだな!」と軽く理解していませんか?(私もその一人でした、偉そうなタイトルですいません...) - ですが、そのような認識では説明できない動作に困惑する時が来るかもしれません
- この記事では、私が実際に犯した勘違いを例に、deferの正しい動作を理解するためのポイントを解説します
前提:deferとは?
前回、deferの基本的な特徴をまとめた記事を書きました。一言で言うと、関数の遅延実行の仕組みです。詳しくはこちらをご覧ください。
私の勘違い
Goのdeferを初めて触れたとき、私はこう思っていました。
deferは関数の終了時に実行される。つまり、関数を抜けた後に一気に処理がされるんだな!!
しかし、この認識は完全に正確ではありません。特にdeferの引数が関数呼び出しを含む場合、この認識だと間違った結果になります。私がその間違いに気づいたのが次のソースコードとなります。
本題:出力結果を予測してみよう
では、私が引っかかったソースコードを見ていきましょう。出力順に①〜④の番号を振っています。これらの番号に対するmessage
の出力値を予測してみてください。
package main
import (
"fmt"
)
// グローバル変数
var message = "original message"
func changeMessage(newMessage string) (resetFunc func()) {
// 現在のメッセージを保存
tmp := message
// 新しいメッセージに変更
message = newMessage
// 元に戻す関数を返す
return func() {
message = tmp
}
}
func main() {
// ①
fmt.Println("Before doSomething:", message)
// main関数の処理
doSomething()
// ④
fmt.Println("After doSomething:", message)
}
func doSomething() {
// ②
fmt.Println("Before defer:", message)
defer changeMessage("deferred message")()
// ③
fmt.Println("After defer:", message)
}
.
.
.
.
.
勘違いしていた時の私の予測
私の頭の中では、deferが最後に実行されるものだと信じていました。doSomething
の最後でmessage
がdeferred message
に変わるんでしょ?と。その為、次のような出力結果を予測していました。
Before doSomething: original message
Before defer: original message
After defer: original message
After doSomething: deferred message
しかし、実際の出力結果は次のようになります。
出力結果
Before doSomething: original message
Before defer: original message
After defer: deferred message
After doSomething: original message
- 「え?
After defer
でmessage
がdeferred message
になっている??」 - 「あれ、
defer
って呼び出し元の関数(今回で言うとdoSomething
)の最後に実行されるんじゃないの??」
はい、当時の私はパニックです。
解説:deferは一番外側の関数を遅延実行させる仕組み
どうしてこのような出力結果となるのか、それは「deferの引数は宣言時に評価される」という特性を理解しきれていなかった為です。
deferに渡される引数が関数の場合、その引数の関数も即時評価(実行)されます。つまり、deferは一番外側の関数を遅延実行させる仕組みということとなります。
改めて、sample.1.go
のdeferの記述を見てみましょう。
defer changeMessage("deferred message")()
この文では内側の関数をchangeMessage("deferred message")
、外側の関数をchangeMessage戻り値の無名関数
と分解できます。
その為、動作としては次のような流れになります。
-
changeMessage("deferred message")
が即時実行される- この時点で
message
の値はdeferred message
に変わります
- この時点で
-
changeMessage戻り値の無名関数
が、deferで遅延実行される関数として登録される -
doSomething
の終了時、deferに登録された無名関数が実行され、message
の値をoriginal message
に戻す
余談:括弧を省略した場合
余談になりますが、deferに渡す関数の実行部分、つまり末尾の括弧を除去すると、どうなるのでしょうか?試してみましょう。
// 省略
func doSomething() {
// ②
fmt.Println("Before defer:", message)
// 末尾の括弧を除去してみる
defer changeMessage("deferred message")
// ③
fmt.Println("After defer:", message)
}
この場合、changeMessage("deferred message")
自身がdeferで遅延実行される関数として登録されます。
Before doSomething: original message
Before defer: original message
After defer: original message
After doSomething: deferred message
まとめ
今回のポイントです。
- deferは一番外側の関数を遅延実行させる仕組みです
- deferは「関数の終わりに実行される」ものではありますが、引数は即時評価されます
- そして、deferに渡される引数が関数の場合、その引数の関数も即時評価(実行)されます
deferを使用する際は、引数の評価タイミングを理解しないと誤解を招くことがありますので注意しましょう!
参考