奇妙な動作をするfor文
まず、最初に奇妙な動作をするコードの例を出します。
以下のコードでは、for文の中でgoroutineを実行している。 main のgoroutineが終了するのを防ぐために sync.WaitGroup
を使用している。 (sync.WaitGroup
を知らない人はGoのWaitGroupを理解するを参考)
func main() {
var wg = &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
これを実行すると、
10
10
10
10
・・・・
10
10
i < 9
と設定しているので、出力されるのは9以下であるはずである。にもかかわらず、10
が出力されて、ループは続行する。
ちなみに、JavaScript でも似たような事象を確認できる。以下コードでは、 setTimeout()という非同期メソッドを使っている。
for (var i = 0; i < 10; i++) {
setTimeout(() => console.log(i))
}
10
10
10
10
・・・・
10
10
解説
for
文は、以下 3 つの主要なコンポーネントで構成される。
- キーワード
for
- たとえば
i := 0; i < foo; i++
のようなステートメント - ループする処理
{}
の中身
goroutineが生成されると、各ループでのスケジューリングのためにキューに入れられる。
一方、for文はgoroutineがスケジュールされて、実行される前に終了する。
for文の最後の実行では、forのステートメント自身はいつ停止する必かが分からない。したがって、i=9 のときでも、インクリメントされる。そして、i=10になると、for文のi < 10の結果が false になり、ループによる本体の実行が停止される。(下図参照)
この奇妙な動作は、Go言語だけではなく、C言語から派生したすべてのプログラミング言語に当てはまる。
Golang では、インデックス変数はポインタとして渡される。
先ほどの、JavaScriptの例では、古いvar
宣言を使用していた。これは、Golang の var
と同様にインデックス変数はポインタとして扱われる。
それに対して、let
はループするごとに変数に更新された値をコピーする。(これをシャドーイングと呼ぶ)
以下はlet
で直した例である。
- for (var i = 0; i < 10; i++) {
+ for (let i = 0; i < 10; i++) {
setTimeout(() => console.log(i))
}
setTimeout()
は非同期であるが、マルチスレッドでは動かないので、0から順番に出力される。
0
1
2
3
4
5
6
7
8
9
Golangでは、以下のようにインデックス変数を goroutine の引数に渡すと解決できる。
var wg = &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
fmt.Print(i)
wg.Done()
}(i)
}
wg.Wait()
並行処理なので、順番は不順だが、0 ~ 9 までの数値が以下のように出力される。
9
4
0
1
2
3
7
5
8
6
また、シャドーイングを使用しても、同じことができる。(mattnさんからコメントいただきました。ありがとうございます。)
func main() {
var wg = &sync.WaitGroup{}
for i := 0; i < 10; i++ {
+ i := i // 外側のiをシャドーイング
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
終わりに
Goの v1.22 でこのGolangの振る舞いについて、改善しつつあるらしいです。
v1.22で最初と同じ以下のコードを実行すると
package main
import (
"fmt"
"sync"
)
func main() {
var wg = &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
9
5
6
4
2
3
7
8
0
1
となる