109
63

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-09-16

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

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

以上です!

109
63
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
109
63

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?