以下のコード例で、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.