Go公式WikiのCommonMistakesの日本語意訳になります。
(元記事の最終更新日: 2014/12/11 rev.2)
他の言語でも起こりますが、ループ内でゴルーチンやクロージャを使う際の気をつけるべきことが書かれています。
よくある間違い
はじめに
新しいプログラマーがGoを使い始めたり、Goプログラマーが新しいコンセプトを使い始めるとき、多くの人々がよくやってしまう間違いがあります。
ここでは網羅的ではありませんが、メーリングリストやIRCでよく出てくる間違いのリストを掲載します。
ループイテレータの変数に対してゴルーチンを使ってしまうこと
Goでイテレーションをする際、データ処理を並列に行うためにゴルーチンを使おうとすることがあります。例えば、次のようなコードを書いてしまうかもしれません。
for val := range values {
go val.MyMethod()
}
またはゴルーチンの中でチャネルからの値を処理するために、
クロージャを使って次のようなコードを書いてしまうかもしれません。
for val := range values {
go func() {
fmt.Println(val)
}()
}
もし上記のコードの問題がすぐに分からなければ、
読み進める前に、何が間違っているのか理解するために少し時間を取りましょう。
上記ループでのval
変数は、実際には各スライスの要素の値を取る単一の変数です。
クロージャは全てその一つの変数にのみバインドされるため、このコードを実行した際には各ループにおいて最後の要素のみ表示されることが確認できるでしょう。
なぜなら、ループが終わるまでゴルーチンが実行されないかもしれないからです。
正しいクロージャのループは以下のように書きます。
for val := range values {
go func(val interface{}) {
fmt.Println(val)
}(val)
}
val
をクロージャへの引数として加えることで、val
は各イテレーションで評価され、ゴルーチンのスタックに積まれるため、各スライスの要素はゴルーチンで最終的に実行される時に利用できます。
もう一つ重要なこととしては、ループ本体で宣言された変数は各イテレーション間で共有されません。したがって、クロージャ内でその変数を別々に使用することができます。
以下のコードは共通のインデックス変数 i
を使って別々のval
を作成しています。これは望ましい結果を返します。
for i := range valslice {
val := valslice[i]
go func() {
fmt.Println(val)
}()
}
クロージャをゴルーチンとして実行しなければ、コードは期待通り実行されることに注意してください。
以下の例は1から10までの整数を出力します。
for i := 1; i <= 10; i++ {
func() {
fmt.Println(i)
}()
}
クロージャは全て同じ変数(この例ではi
)ですが、これらは変数が変わる前に実行されるため、期待通りの振る舞いになります。
http://golang.org/doc/go_faq.html#closures_and_goroutines
comments powered by Disqus