Go
golang

Goのforとgoroutineでやりがちなミスとたった一つの冴えたgo vetと

More than 3 years have passed since last update.

分かっていたつもりなのにまたやってしまったので、自戒を込めて書いておきます。


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を開始しても直ちにスイッチされるわけではなく、


  1. 先にループ処理が終了する

  2. valの値は最後の値(ここでは9)になる

  3. goroutineにスイッチされ実際に実行される

  4. 各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を使っていたのでチェックされてなかった・・・)

こんな感じです。

222ad348baa59941f7eb77861fd58cc1.png

以上です!