[インデックス 13765] ファイルの概要
このコミットは、Go言語のFAQドキュメント(doc/go_faq.html
)に対して行われたもので、クロージャがループ変数をキャプチャする際の一般的な問題に対する、より簡潔な解決策を提示しています。具体的には、ループ内で新しい変数を宣言することで、各イテレーションで変数の独立したコピーをクロージャにバインドする方法を追加説明しています。
コミット
- コミットハッシュ:
0cab7d52d5ffaa23f31dfcabf61662a6581d1edb
- 作者: Rob Pike r@golang.org
- 日付: Fri Sep 7 09:11:39 2012 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0cab7d52d5ffaa23f31dfcabf61662a6581d1edb
元コミット内容
faq: another way to solve the closure/variable/range complaint
It's easier just to declare a new variable.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6501103
変更の背景
Go言語において、for
ループ内でゴルーチン(またはクロージャ)を起動し、ループ変数を参照する場合、よくある落とし穴として「ループ変数のキャプチャ問題」があります。これは、クロージャが変数の「値」ではなく「変数そのもの」をキャプチャするため、ループが終了した時点での変数の最終的な値がすべてのクロージャで共有されてしまうというものです。
この問題に対する従来の解決策としては、クロージャに引数としてループ変数を渡す方法がFAQに記載されていました。しかし、このコミットは、より簡潔でGoらしい解決策として、ループの各イテレーション内で新しい変数を再宣言する方法をFAQに追加することで、開発者がこの一般的な問題をより容易に回避できるようにすることを目的としています。
前提知識の解説
1. Go言語のクロージャ (Closures)
Go言語におけるクロージャは、それが定義された環境(レキシカルスコープ)の変数を参照できる関数リテラルです。クロージャは、その定義時に存在していた変数を「記憶」し、その変数がクロージャの実行時に変更されても、その変更を反映します。
2. ゴルーチン (Goroutines)
ゴルーチンはGo言語の軽量な並行処理の単位です。関数呼び出しの前にgo
キーワードを付けることで、その関数は新しいゴルーチンとして実行されます。ゴルーチンは同じアドレス空間を共有するため、共有データへのアクセスには注意が必要です。
3. ループ変数のキャプチャ問題 (Loop Variable Capture Problem)
Go言語のfor
ループ、特にfor range
ループでゴルーチンやクロージャを使用する際に頻繁に発生する問題です。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
values := []int{1, 2, 3}
var wg sync.WaitGroup
for _, v := range values {
wg.Add(1)
go func() {
defer wg.Done()
// ここでvを参照
fmt.Println(v)
}()
}
wg.Wait()
time.Sleep(time.Millisecond) // ゴルーチンが実行されるのを待つ
}
上記のコードを実行すると、期待される出力(1, 2, 3)ではなく、しばしば3 3 3
のような出力が得られます。これは、クロージャがループ変数v
そのものを参照しているためです。ループが非常に高速に実行されるため、すべてのゴルーチンが起動される前にループが終了し、v
の最終的な値(この場合は3
)がすべてのクロージャに共有されてしまうためです。
技術的詳細
このコミットが提案する解決策は、Go言語の変数のスコープとシャドーイングの特性を巧みに利用したものです。
従来の解決策は、クロージャの引数としてループ変数の値を渡すことでした。
for _, v := range values {
go func(u int) { // vの値をuとしてキャプチャ
fmt.Println(u)
}(v) // ここでvの現在の値が渡される
}
この方法は有効ですが、コミットが提案する新しい方法は、ループの各イテレーション内でループ変数と同じ名前の新しい変数を宣言することです。
for _, v := range values {
v := v // ここで新しい'v'が宣言され、現在のループ変数の値で初期化される
go func() {
fmt.Println(v) // この'v'は、新しく宣言されたローカルな'v'を参照する
}()
}
このv := v
という記述は一見すると冗長に見えますが、Go言語の仕様上、これは新しい変数の宣言と初期化を意味します。
for _, v := range values
のv
は、ループの各イテレーションで再利用される単一の変数です。v := v
の左側のv
は、現在のスコープ(この場合はfor
ループのブロック内)で新しく宣言される変数です。右側のv
は、for range
ループによって提供された現在のイテレーションのv
の値を参照します。
これにより、ループの各イテレーションで、そのイテレーションに固有のv
のコピーが作成されます。クロージャはこの新しく作成されたローカルなv
をキャプチャするため、各ゴルーチンはループのそのイテレーション時点での正しい値を持つことになります。これは、Goの変数のシャドーイング(内側のスコープで外側のスコープの変数と同じ名前の変数を宣言すること)の典型的な例であり、この文脈では非常に効果的な解決策となります。
コアとなるコードの変更箇所
doc/go_faq.html
ファイルの変更点です。
--- a/doc/go_faq.html
+++ b/doc/go_faq.html
@@ -1220,8 +1220,9 @@ but <code>v</code> may have been modified since the goroutine was launched.\n </p>\n \n <p>\n-To bind the value of <code>v</code> to each closure as they are launched, one\n-could modify the inner loop to read:\n+To bind the current value of <code>v</code> to each closure as it is launched, one\n+must modify the inner loop to create a new variable each iteration.\n+One way is to pass the variable as an argument to the closure:\n </p>\n \n <pre>\n@@ -1239,6 +1240,21 @@ anonymous function. That value is then accessible inside the function as\n the variable <code>u</code>.\n </p>\n \n+<p>\n+Even easier is just to create a new variable, using a declaration style that may\n+seem odd but works fine in Go:\n+</p>\n+\n+<pre>\n+ for _, v := range values {\n+ <b>v := v</b> // create a new \'v\'.\n+ go func() {\n+ fmt.Println(<b>v</b>)\n+ done <- true\n+ }()\n+ }\n+</pre>\n+\n <h2 id=\"Control_flow\">Control flow</h2>\n \n <h3 id=\"Does_Go_have_a_ternary_form\">\n```
## コアとなるコードの解説
変更は、`doc/go_faq.html`の「Why do my goroutines sometimes not run in the order I expect?」セクション(または類似のクロージャとループ変数の問題に関するセクション)に追加されています。
追加されたHTMLスニペットは以下のGoコード例を示しています。
```go
for _, v := range values {
v := v // create a new 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}
このコードブロックの核心は v := v
の行です。
for _, v := range values
で宣言されたv
は、ループの各イテレーションで新しい値が割り当てられますが、メモリ上は同じ変数インスタンスを指します。v := v
の行は、現在のループイテレーションのスコープ内で、新しい変数v
を宣言し、その値を外側のスコープのv
(つまり、現在のイテレーションのループ変数の値)で初期化します。- これにより、各イテレーションで独立した
v
のコピーが作成され、そのコピーがクロージャによってキャプチャされます。結果として、ゴルーチンが実行される際に、それぞれのクロージャが起動された時点でのv
の正しい値を出力するようになります。
この変更は、GoのFAQドキュメントを更新し、この一般的な問題に対するより簡潔でイディオマティックな解決策を公式に推奨するものです。
関連リンク
- Gerrit Change-Id:
https://golang.org/cl/6501103
(GoプロジェクトのコードレビューシステムであるGerritへのリンク) - Go Wiki - CommonGotchas: https://go.dev/wiki/CommonGotchas#using-goroutines-on-loop-iterator-variables (Goの一般的な落とし穴に関する公式Wikiページ。この問題について詳しく解説されています。)
参考にした情報源リンク
- Go言語の公式ドキュメントおよびFAQ (
doc/go_faq.html
の変更内容) - Go Wiki - CommonGotchas: https://go.dev/wiki/CommonGotchas#using-goroutines-on-loop-iterator-variables
- Go言語におけるクロージャとループ変数のキャプチャに関する一般的な知識。