Go言語クロージャーについての豆知識


背景

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