golangのdeferで呼ぶメソッドのレシーバに関する注意点

エンジニア

golangでコードを書いていてdeferで呼び出しているあるメソッド内で参照しているメンバ変数が、別のポインタレシーバ経由で書き換えたはずの値に書き換わっていないことで気づいた注意点。

deferで呼び出したメソッドの挙動が意図通りにならない

下記のように、

  • あるExampleClientという構造体があったとする
  • 一度Join()メソッドを呼び出すとメンバ変数joinedtrueになる
    • 当然このメソッドのレシーバは内容を変更するためにポインタレシーバである
  • 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() が呼び出されたときはjoinedtrueになっているものと思っていた。

が、実際は違ってjoinedfalseのままだった。

原因: 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")
  }
}

コメント

タイトルとURLをコピーしました