ふわっとテック日記

テック系のことをメインに書いていきます。go言語/typescript/javascriptが好きです。たまに趣味(筋トレ)の話や日常系の記事も書きたいな〜と思っています。

【Go1.21】ループ処理の新機能

8月にリリースされたGo1.21のループ処理の新仕様についての記事です。

Go1.21のリリースノートはこちら

tip.golang.org


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という仕組みです。

github.com


仕組みは非常に単純で、

  • ビルドコマンドや 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の機能を使用してみることをお勧めします。