Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 valuesvは、ループの各イテレーションで再利用される単一の変数です。
  • 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 &lt;- 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ドキュメントを更新し、この一般的な問題に対するより簡潔でイディオマティックな解決策を公式に推奨するものです。

関連リンク

参考にした情報源リンク