どうも、駆け出しGopherです。
今回は、forループ内でgoroutine(ゴルーチン)を使う場合にGo言語初心者がハマりがちな罠について書きます。
結論から書くと
結論
○ 正常に動く例 通常の関数をgoroutineで実行
特に何も意識する必要のないケースです。
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func main() { names := []string{"Taro", "Ziro", "Saburo"} for _, name := range names { wg.Add(1) go sayHello(name) } wg.Wait() fmt.Println("DONE") } func sayHello(name string) { defer wg.Done() fmt.Printf("Hello, %s\n", name) }
実行結果は想定通り
実行結果
Hello, Ziro
Hello, Taro
DONE
× 正常に動かない例 クロージャーをgoroutineで実行
このパターンは注意が必要!!
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func main() { names := []string{"Taro", "Ziro", "Saburo"} for _, name := range names { wg.Add(1) go func() { defer wg.Done() fmt.Printf("Hello, %s\n", name) }() } wg.Wait() fmt.Println("DONE") }
一見、問題なさそうですが「Hello, Saburo」しか出力されません。
実行結果
Hello, Saburo
Hello, Saburo
DONE
この例では、goroutineは反復変数 name
を囲むクロージャーを実行しています。
goroutineの実行は任意のタイミングで行われるので、実行時の name
の値は不確定。
たいていのマシンではgoroutineが開始される前にforループが終了してしまいます。
つまり、何が起こっているかと言うと
注意ポイント
→ name 変数はスコープ外になる。
→ Goのランタイムは name が参照されているのを把握していて、参照できるようにヒープに移す。このとき、最後の値である「Saburo」への参照を保持したまま
→ 「Hello, Saburo」のみが出力される
go vet を使うと怪しいコードを指摘してくれて、「./prog.go:16:30: loop variable name captured by func literal 」と警告してくれます。
○ 正しく動くように修正
name 変数のコピーをクロージャーに渡して、forループ終了後も意図している値を保持できるようにする。
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func main() { names := []string{"Taro", "Ziro", "Saburo"} for _, name := range names { wg.Add(1) go func(name string) { defer wg.Done() fmt.Printf("Hello, %s\n", name) }(name) } wg.Wait() fmt.Println("DONE") }
実行結果は、
実行結果
Hello, Ziro
Hello, Taro
DONE
引数としてnameを渡すことでname(ここでは文字列)のコピーが行われ、goroutine実行時に適切な値を参照することができるようになりました。
めでたし、めでたし!!
参考図書
Tour of Goを終えた人にオススメです!!