概要
久し振りにタイトルのミスをやってしまったので、忘れないようにメモです。
よく言われていることなんですが、ふとしたときにやってしまうんですよね・・・。
Goのループ変数は使いまわしされる
Goでは、ループ変数は使いまわしされます。つまり、ループの前にループ変数が定義されて、ループが回るたびに値をそこに入れていってくれます。毎回変数を定義してくれているのではなくて、同じ変数に値を使いまわしで入れていくので、ループ変数のアドレスはループしてる間ずっと同じです。
これ、よくある質問みたいで、GoのWikiにもかいてあります。
ループ内でゴルーチンを使う際に、ループ変数を渡したりすることはよくあります。
で、ループ変数の値がなんらかの構造体の場合に、ポインタで渡すこともよくあります。
このときに、コピー取らないでそのまま渡すと、意図せぬ動きになるってことです。
回避策は?
回避策は上のGoのWikiにも書いてありますが、ループ内でコピーをとって、それをポインタで渡すことですね。
それか、以下の記事にあるように
添字をつかって、ちゃんと元のスライスなり配列から取得して、それを渡すとうまくいきます。
サンプル
以下、ループ内でゴルーチンを起動してて、そこにループ変数をポインタで渡すサンプルです。
package loopiterator import ( "sync" "github.com/devlights/gomy/output" ) type ( value struct { i int } ) // PassingLoopVariableToGoroutineByPointer -- ループ変数をポインタ経由でGoroutineに渡した場合のサンプルです. // // REFERENCES: // - https://stackoverflow.com/a/23637430 // - https://golang.org/doc/effective_go.html#channels func PassingLoopVariableToGoroutineByPointer() error { // ---------------------------------------------------------------- // Go では、ループ変数は使いまわしされるので // 例えば、ループ内でGoroutineを起動する際にループ変数を // ポインタで渡すような書き方をすると、実際には同じアドレスを // 渡していることになる。 // // なので、各Goroutineは、ほぼ同じ値を見ることになってしまう。 // (Goroutineがとても早く起動して、変化する前の値をみた場合はその時の値が見える) // // 回避策としては // 渡す前にコピーをとって渡すようにするか、添字を使って // 値を取得するようにすれば大丈夫 // ---------------------------------------------------------------- badpattern() output.StdoutHr() goodpattern() return nil } func badpattern() { var ( wg = &sync.WaitGroup{} vals = []value{ {1}, {2}, {3}, } ) wg.Add(len(vals)) for _, v := range vals { output.Stdoutf("[bad][v]", "addr=%p, value=%v\n", &v, v) go func(v *value) { defer wg.Done() output.Stdoutf("[bad][v][goroutine]", "addr=%p, value=%v\n", v, *v) }(&v) } wg.Wait() } func goodpattern() { var ( wg = &sync.WaitGroup{} vals = []value{ {1}, {2}, {3}, } ) wg.Add(len(vals)) for _, v := range vals { output.Stdoutf("[good][v]", "addr=%p, value=%v\n", &v, v) // 渡す前にコピーを取得 // - https://stackoverflow.com/a/23637430 v := v go func(v *value) { defer wg.Done() output.Stdoutf("[good][v][goroutine]", "addr=%p, value=%v\n", v, *v) }(&v) } wg.Wait() }
実行すると以下のようになります。
$ make run go run github.com/devlights/try-golang/cmd/trygolang -onetime -example "" ENTER EXAMPLE NAME: passing [Name] "passing_loop_variable_to_goroutine_by_pointer" [bad][v] addr=0x40000b0708, value={1} [bad][v] addr=0x40000b0708, value={2} [bad][v] addr=0x40000b0708, value={3} [bad][v][goroutine] addr=0x40000b0708, value={3} [bad][v][goroutine] addr=0x40000b0708, value={3} [bad][v][goroutine] addr=0x40000b0708, value={3} -------------------------------------------------- [good][v] addr=0x40000260c8, value={1} [good][v] addr=0x40000260c8, value={2} [good][v] addr=0x40000260c8, value={3} [good][v][goroutine] addr=0x40000260f0, value={3} [good][v][goroutine] addr=0x40000260e8, value={2} [good][v][goroutine] addr=0x40000260e0, value={1} [Elapsed] 4.018291ms
予想通り、コピー取らずに処理してる方は、ゴルーチン側で出力されている値が全部同じですね。
上の実行では、キレイに全部同じになりましたが、場合によっては以下のようになったりします。
$ make run go run github.com/devlights/try-golang/cmd/trygolang -onetime -example "" ENTER EXAMPLE NAME: passing [Name] "passing_loop_variable_to_goroutine_by_pointer" [bad][v] addr=0x40000b0708, value={1} [bad][v][goroutine] addr=0x40000b0708, value={2} [bad][v] addr=0x40000b0708, value={2} [bad][v] addr=0x40000b0708, value={3} [bad][v][goroutine] addr=0x40000b0708, value={3} [bad][v][goroutine] addr=0x40000b0708, value={3} -------------------------------------------------- [good][v] addr=0x40000b0730, value={1} [good][v] addr=0x40000b0730, value={2} [good][v] addr=0x40000b0730, value={3} [good][v][goroutine] addr=0x40000b0748, value={3} [good][v][goroutine] addr=0x40000b0740, value={2} [good][v][goroutine] addr=0x40000b0738, value={1} [Elapsed] 2.909833ms
bad の方に一個 value が 2 ってなってるやつがいますね。これは、このゴルーチンがたまたま早く動いたので、そのときの値を表示しているからです。このゴルーチンがそもそも表示するべき値が 2 の場合だったら、たまたまオッケイですが、それは保証もされていないし、このゴルーチンが2を表示するのが正しいのかも分からないので、どのみちダメです。
このようなバグって、開発してる際はちゃんと動いているように見えるのが困ります。。。で、大体本番環境でおかしくなります。。些細なバグですが、原因として見つけるのはとても困難です。気をつけないといけない。
また、上ではゴルーチンを使っていますが、別にゴルーチンを使わなくても当然ですが発動できます。
以下は、最初にスライス定義しておいて、ループ内でループ変数をポインタで保持していくパターンです。
何らかの条件にマッチするデータのみを抽出したりするのは、よくやる処理ですね。
package loopiterator import ( "strconv" "github.com/devlights/gomy/output" ) // CommonMistakePattern は、Goにてループ変数を扱う際によくある間違いについてのサンプルです. // // REFERENCES:: // - https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable func CommonMistakePattern() error { // ------------------------------------------------------------------------- // Goのループ変数は他の言語に無いクセがあり、ループ毎にループ変数を割り当てて // 処理するのではなく、ループ全体で一つの値を使い回すようになっている。 // (変数のポインタは同じで値だけが変わっていく) // // なので、上記のWikiにあるようにポインタを保持するスライスなどを持っている状態で // ループ変数のポインタをそのまま格納するようなことをしてしまうと、最終的に全部同じ // データになってしまう。(ポインタが同じなので、ループの最終番目のデータになっている) // // 解決方法は、利用する前にループ変数の「コピー」をちゃんと取ること。 // コピーを格納するようにしておけば、同じポインタになってしまうことは無い。 // ------------------------------------------------------------------------- bad() output.Stdoutl("--------------------------------------------------") good() return nil } func bad() { var ( items []*int ) for i := 0; i < 5; i++ { // コピーを取らずにループ変数のポインタを格納している // なので、結局全部同じものを格納していることになる items = append(items, &i) } for i, v := range items { output.Stdoutf(strconv.Itoa(i), "p=%p\tv=%v\n", v, *v) } } func good() { var ( items []*int ) for i := 0; i < 5; i++ { // 格納する前にループ変数のコピーを作っている // なので、ループ毎にちゃんと異なる値となる iCopy := i items = append(items, &iCopy) } for i, v := range items { output.Stdoutf(strconv.Itoa(i), "p=%p\tv=%v\n", v, *v) } }
try-golang/using_reference_to_loop_iterator_variable.go at master · devlights/try-golang · GitHub
実行すると以下のようになります。
$ make run go run github.com/devlights/try-golang/cmd/trygolang -onetime -example "" ENTER EXAMPLE NAME: using [Name] "using_ref_to_loop_iterator_variable" 0 p=0x400012a700 v=5 1 p=0x400012a700 v=5 2 p=0x400012a700 v=5 3 p=0x400012a700 v=5 4 p=0x400012a700 v=5 -------------------------------------------------- 0 p=0x400012a708 v=0 1 p=0x400012a710 v=1 2 p=0x400012a718 v=2 3 p=0x400012a720 v=3 4 p=0x400012a728 v=4 [Elapsed] 356.709µs
おすすめ書籍
自分が読んだGo関連の本で、いい本って感じたものです。
- 作者:Katherine Cox-Buday
- 発売日: 2018/10/26
- メディア: 単行本(ソフトカバー)
- 作者:松尾 愛賀
- 発売日: 2016/04/15
- メディア: 単行本(ソフトカバー)
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- 作者:Alan A.A. Donovan,Brian W. Kernighan
- 発売日: 2016/06/20
- メディア: 単行本(ソフトカバー)
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場