【Go1.21】ループ処理の新機能
8月にリリースされたGo1.21のループ処理の新仕様についての記事です。
Go1.21のリリースノートはこちら
Go言語のループでは
for i := range ~~~
や for i := 0; i < MAX; i++ {
といったような形をよく使うと思います。
ここで使ってる変数iはループ変数と呼ばれるものですが、
go1.21からこのループ変数に新しい仕様が実装されました。
今までループ変数は1回初期化されるとループ処理全体で使いまわされるという仕様でした。
つまりループ処理での1回目のイテレーションから最後のイテレーションまで、
変数は同じアドレスのものがずっと使用されるということですね。
こういった仕様によりバグが生じることはそれほど頻繁にはないのですが、
それでもコードを書いた人の想定通りに動かないということが稀に起こっています。
それが、下記のようなケースです。
- コードサンプル1
func main() { for i := 0; i < 5; i++ { f := func() { fmt.Println("This is number", i) } go loopFunc(f) } } func loopFunc(f func()) { f() } (ゴルーチンの排他制御コードは省略)
このコードを書いた人はおそらく、1~5の番号の出力がそれぞれ1度ずつ出力されると想定していることでしょう。↓↓
This is output 1 This is output 2 This is output 3 This is output 4 This is output 5 (順不同)
しかし実際の出力は次のようになります。↓↓
This is output 5 This is output 5 This is output 5 This is output 5 This is output 5
なぜでしょう?
冒頭で触れた通り、ループ変数はループ処理を通じて使いまわされます。
各イテレーションでゴルーチンとしてloopFunc関数を呼び出していますが、これらのゴルーチンが実行される頃にはループ処理は終わっています。
つまりループ変数iには、最後のイテレーション時の値である5が入っているのです。
こうして、出力時の数値は全て5になってしまうというわけです。
これを回避するための策として今までよく用いられてきたのが、イテレーション内でループ変数の値を別の変数にコピーして使う方法です。
↓こんな感じです。
- コードサンプル2
func main() { for i := 0; i < 5; i++ { copied := i f := func() { fmt.Println("This is output", copied) } go loopFunc(f) } } func loopFunc(f func()) { f() } (ゴルーチンの排他制御コードは省略)
こうすることでループ変数の参照渡しを防げるのでシステムバグの防止になります。
ただこの値コピーをいちいち実装するのは非常に煩雑です。
そこでGo1.21において、
ループ変数をループ処理単位ではなく、イテレーションごとに作成することを選択できるようになりました。
それがこちらのLoopvarExperimentという仕組みです。
仕組みは非常に単純で、
- ビルドコマンドや
go run
コマンドに接頭辞GOEXPERIMENT=loopvar
をつけてコードを実行する
だけで、ループ変数がイテレーションごとに作成されるようになり、
コードサンプル1のようなコードが想定通りに動くようになります。
GOEXPERIMENT=loopvar go install mypackage GOEXPERIMENT=loopvar go build mypackage GOEXPERIMENT=loopvar go test mypackage GOEXPERIMENT=loopvar go run mycode.go
↓GOEXPERIMENT=loopvarを使ったコードサンプル1の実行結果↓
This is output 0 This is output 3 This is output 4 This is output 2 This is output 1
この仕組みは現段階では暫定的な導入であるため、GOEXPERIMENTの指定がなければ今まで通りの挙動となります。
Googleによると、この方法でのコード実行によって、既存のシステムがエラーになるということはほぼほぼあり得ないようです。
Google曰く、「2023の5月初頭からこの新しいループをプロダクションツールチェーンに適用しているが、1つの問題も報告されていない」そうです。
forループのクロージャが引き起こすバグは、複雑なシステムを運用しているとデバッグも難しく、非常に厄介なものとなります。
ぜひこのLoopvarExperimentの機能を使用してみることをお勧めします。