分かっていたつもりなのにまたやってしまったので、自戒を込めて書いておきます。
forループ内でgoroutineを使う場合の注意点
for内でgoroutineを実行する際にやりがちなミスがあります。
以下のコードを見てください。
package main
import (
"fmt"
"time"
)
func main() {
values := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
for _, val := range values {
go func() {
fmt.Println(val)
}()
}
// goroutineが終了するまで待つ。
// コード簡素化のためにsleepしているだけだが、
// 本来はsync.WaitGroupとかchannel使うなど適切な処理をすべき。
time.Sleep(1 * time.Second)
}
0から9までの値をループして、goroutine内で表示するだけです。
当然0から9まで表示される気がします。
問題なくコンパイルもされるので、実行してみましょう。
$ go run main.go
9
9
9
9
9
9
9
9
9
9
あれ?
なんと9しか表示ません!!!(表示内容は実行環境によって異なる場合があるかも)
なんでこうなるかというと、
for _, val := range values {
go func() {
fmt.Println(val) // <-ここ!!
}()
}
ここでgoroutineに渡しているval
という値は、ループされる度に作られる変数ではなく、
ループ全体で使われる変数だからです。
そしてgoroutineを開始しても直ちにスイッチされるわけではなく、
- 先にループ処理が終了する
- valの値は最後の値(ここでは9)になる
- goroutineにスイッチされ実際に実行される
- 各goroutineは最後の値になったvalを使って処理を行う。
というフローになるため、上記のような結果になるわけです。
ところで、先に実行環境によっては結果が変わるかもと書きましたが、
もしforループが完了する前まにgoroutineにスイッチされるような場合があれば、
その時点でのvalの値が表示されるはずだからです。
ただ現実にはそんな事はほとんど無さそうです。
対処方法
以下の2つ(と一つの亜種)が主な対処方法になるかと思います。
ループ内で閉じた変数に束縛する
goroutine開始前に各ループ内で閉じた変数を宣言し、それに値をコピーして使います。
func main() {
values := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
for _, val := range values {
i := val // 別の変数にコピーする
go func() {
fmt.Println(i) // 実際の処理ではコピーされた変数を使う
}()
}
time.Sleep(1 * time.Second)
}
もっともシンプルです。
ただ、値をコピーするだけの変数があるのがなんとなくカッコ悪い気がします(個人の感想です)。
無名関数で引数を宣言し、実行時に値を渡す
func main() {
values := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
for _, val := range values {
go func(i int) { // 引数を追加
fmt.Println(i)
}(val) // 関数実行時に現在の値を渡す
}
time.Sleep(1 * time.Second)
}
こうしておけばgoroutine内で使われる値を意図したものに束縛することができます。
引数宣言が面倒で見た目にゴチャついていますが、なんとなくカッコいい気がします(個人の感想です)。
実行関数を別に宣言する。
func main() {
values := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 実行関数を別に用意
f := func(i int) {
fmt.Println(i)
}
for _, val := range values {
go f(val) // 先に用意した関数を実行
}
time.Sleep(1 * time.Second)
}
やってる事は先と同じですが、見た目のゴチャつき感が緩和されている気がします。
対処後
いずれの対処方法でも、実行後は以下のような結果で意図通りの動作となります。
$ go run main.go
0
6
7
5
8
3
9
4
1
2
でも気づかないよ?
対処方法は分かりましたが、このミスの根本的に問題なところは、
コンパイルも通るし、実行時エラーにもならなかったりするところです。
そしてよく分からない実行結果に戸惑うわけです。
ただ気をつけるしかないのでしょうか?
そんな事はありません。Goらしく、toolingで解決可能です。
そして go vet
go vetはgoに最初から入っているコマンドで、
怪しげなコードを探してきて"なんかこれエラーっぽいよ?"と指摘してくれるツールです。
とりあえず最初の問題があるコードで試してみましょう。
$ go vet main.go
main.go:13: range variable val captured by func literal
exit status 1
「range変数のvalが関数リテラル内で使われてるよ」みたいな感じでしょうか。
これで何か問題ありそうと気づくことができるので、先の「気づかない」という最大の問題に対処可能です。
ただ、毎度コマンドを実行するのも面倒なので、エディタで自動的にチェックするようにしておきましょう。
例えばEmacsならflycheckを使えば勝手にgo vetでのチェックも行ってくれます。
(自分は最近までflymakeを使っていたのでチェックされてなかった・・・)
こんな感じです。
以上です!