golangでコードを書いていてdeferで呼び出しているあるメソッド内で参照しているメンバ変数が、別のポインタレシーバ経由で書き換えたはずの値に書き換わっていないことで気づいた注意点。
deferで呼び出したメソッドの挙動が意図通りにならない
下記のように、
- ある
ExampleClient
という構造体があったとする - 一度
Join()
メソッドを呼び出すとメンバ変数joined
がtrue
になる- 当然このメソッドのレシーバは内容を変更するためにポインタレシーバである
Close()
メソッドを呼び出したときにjoined
がtrueのときだけリソースのクローズ処理をする- (ここでは”do close”を表示)
- main関数で上記を実行する。deferでClose()を呼ぶようにしてある
type ExampleClient struct {
joined bool
}
// 値を変更するのでポインタレシーバにしている
func (c *ExampleClient) Join() {
c.joined = true
}
// 値を変更しないのでポインタレシーバにしていない
func (c ExampleClient) Close() {
if c.joined {
fmt.Println("do close")
}
}
func main() {
c := &ExampleClient{}
defer c.Close()
c.Join()
}
Go Playground - The Go Programming Language
このようなコードを書いていて、上記のmain関数を実行すると、c.Join
を呼び出しているので、当然defer c.Close()
が呼び出されたときはjoined
がtrue
になっているものと思っていた。
が、実際は違ってjoined
はfalse
のままだった。
原因: deferの呼び出しの引数は即時評価
A Tour of Go
The deferred call’s arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
deferされた呼び出しの引数は即時評価されるが、その関数の呼び出しは(deferを)包含する関数がreturnするまで実行されない。
A Tour of Go
つまり、defer c.Close()
の行が実行された時点で値は決まってしまうため、あとから書き換わっても影響を与えないようだ。
対処
ひとまずワークアラウンドとしてClose()
メソッドのレシーバをポインタレシーバに変更することで期待した動作になったが、この場合はどうするのが正解なのだろうか。
func (c *ExampleClient) Close() {
if c.joined {
fmt.Println("do close")
}
}
コメント