profile-image

masatora.net

Golangのループ内でクロージャを定義して使用する場合の注意点

Go

3行で

  • Golang1.21まででは、ループ変数は同じアドレスに値が格納される。
  • そのため、ループ内でクロージャを定義して呼び出す等の特定のケースで、全てのループで毎回同じ値になることがある。
  • これは、ループ内で変数を定義し直すのが1.21までの標準的な解決策。1.22で修正される予定らしい。1.21でもexperimentalで使える

Go1.21までの仕様と発生しうる問題

以下のコード例で、iの値とアドレスが表示されるが、Go1.21までの挙動では全てのイテレーションでiのアドレスが同じである。これがクロージャを使用した際に問題を引き起こす。

package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        fmt.Printf("iの値: %d, iのアドレス: %p\n", i, &i)
    }
}

https://go.dev/play/p/5X09tR5MNec

iの値: 0, iのアドレス: 0xc000012028
iの値: 1, iのアドレス: 0xc000012028
iの値: 2, iのアドレス: 0xc000012028

Program exited.

そのため、ループ内でクロージャを定義して呼び出す等のケースでは、全てのループで毎回同じ値になってしまう。これは気づきにくいバグの原因になりがちだと思う。

package main

import "fmt"

func main() {
	var funcs []func()

	for i := 0; i < 3; i++ {
		funcs = append(funcs, func() {
			fmt.Println(i)
		})
	}

	for _, f := range funcs {
		f()
	}
}

https://go.dev/play/p/hn-5RCih2yP

3
3
3

Program exited.

その他同様の問題が発生しうるケース

クロージャだけでなく、goroutineやt.parallelを使用する場合にも同様の問題が生じ得る。これらの状況でも変数のスコープには注意が必要である。

対応策

この問題Goのバージョン1.22で修正されることが予定されている。それまでは、イテレーションごとにループ変数をローカル変数に代入し直すことで問題を解決できる。

package main

import "fmt"

func main() {
	var funcs []func()

	for i := 0; i < 3; i++ {
		i := i // ここで i のコピーを作成する
		funcs = append(funcs, func() {
			fmt.Println(i)
		})
	}

	for _, f := range funcs {
		f()
	}
}

https://go.dev/play/p/JSWkYlucoZs

0
1
2

Program exited.

参考