Go CommonMistakes (Japanese)

"CommonMistakes" from golang wiki

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