背景###
for分の中に関数を書くときに、特にfor分のスコープと関数の組み合わせによって、クロージャーが生成され、関数の実行タイミングはfor分と一致ではない場合は、バグは発生しやすいです。
例えば、javascriptでは以下のようなクロージャー典型例があります。
Javascriptの豆知識(let,var,クロージャーに関する面接問題)
Javascriptではletという変数宣言方法を活かすことによって、うまくfor分共にあるクロージャー問題点を解消できますが、Go言語だと、どうやって解消できますでしょうか
問題####
コード#####
func main() {
n := 5
funcs := []func(){}
for i:=0;i<n;i++ {
// fmt.Println(&i) // コメントアウトを外してみたら、iのメモリアドレスは同じものです。
funcs = append(funcs, func() {
fmt.Print(i)
})
}
for i:=0;i<n;i++ {
funcs[i]()
}
}
出力#####
5
5
5
5
5
原因#####
原因は二つがあります
- これはfor分からループするタイミング(funcsスライスに関数を作って入れるタイミング)とfuncsスライスにある関数が実行されるタイミングは違います。
- funcスライスにあるすべてな関数にある変数iは同じところに参照しています。(for分ではメモリアドレスが同じなiを使っているためです)
- そのため、関数が実行するタイミングでは、すべてな関数から参照しているiはすでに5になりましたので、すべてな関数は同じi(10)を出力しました。
javascriptだと、for分にiをletで定義することによって、問題を解消できますが、Go言語はどうなるですか><?
いろいろ調べました。
解決方法1####
新しい変数iを作ることによって、新たなiをメモリ場で開拓し、funcsスライスに入れる関数はこちらのiを記憶します。
コード#####
func main() {
n := 5
funcs := []func(){}
for i:=0;i<n;i++ {
i := i
// fmt.Println(&i) //コメントアウトを外して見たら、iのメモリアドレスはそれぞれ異なります。
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for i:=0;i<n;i++ {
funcs[i]()
}
}
出力#####
0
1
2
3
4
解決方法2####
実行された関数Aが関数Bをreturnします、関数Bをfuncsスライスに入れます。関数Aに渡した引数は値渡しなので、関数Aが実行されるときに、iが新しく作られています。
これによって、関数Bは関数Aとクロージャーが生成され、関数Aの中に新たなiがメモリ場で開拓され、関数Bがそのiを参照します。それぞれの関数Bはそれぞれのiを参照しています。
コード#####
func main() {
n := 5
funcs := []func(){}
for i:=0;i<n;i++ {
funcs = append(funcs, func(i int) func() {
// fmt.Println(&i) // コメントアウトを外してみたら、iのメモリアドレスはそれぞれ異なります。
return func() {
fmt.Println(i)
}
}(i))
} // ここ → 関数Aにforループのiを渡し、関数Bを返されます。
for i:=0;i<n;i++ {
funcs[i]()
}
}
出力#####
0
1
2
3
4
ちなみに、関数Aに引数をポインター渡しとして実行したら、どうなりますか?
for i:=0;i<n;i++ {
funcs = append(funcs, func(i *int) func() {
fmt.Println(i)
return func() {
fmt.Println(*i)
}
}(&i))
}
当然、すべてなiが同じメモリアドレスに参照し、全てのfuncの出力は同じ5になります。
0xc00001c080
0xc00001c080
0xc00001c080
0xc00001c080
0xc00001c080
5
5
5
5
5
参考資料###