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

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

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

以上です!


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。