LoginSignup
5
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-04

背景

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)を出力しました。 closure scope.001.jpeg

javascriptだと、for分にiをletで定義することによって、問題を解消できますが、Go言語はどうなるですか><?
いろいろ調べました。

解決方法1

新しい変数iを作ることによって、新たなiをメモリ場で開拓し、funcsスライスに入れる関数はこちらのiを記憶します。
closure scope.003.jpeg

コード
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を参照しています。
closure scope.002.jpeg

コード
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

参考資料

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2