[インデックス 17728] ファイルの概要
このコミットは、Go言語の公式ドキュメントの一部である doc/effective_go.html
ファイルに対する変更です。具体的には、effective_go.html
内のサーバー例において、ゴルーチン間で変数を共有する際の一般的なバグとその解決策を説明するセクションが修正・追記されています。このドキュメントは、Go言語を効果的に書くためのプラクティスやイディオムを解説するものであり、今回の変更は特に並行処理における変数のスコープとキャプチャに関する重要な教訓を提供します。
コミット
このコミットは、doc/effective_go.html
内のサーバー例におけるゴルーチン間の変数共有に関するバグを修正し、その解決策を教育的な例として追加することを目的としています。Goの for
ループ内でゴルーチンを起動する際に発生しがちな、ループ変数の再利用による意図しない変数共有の問題を明確にし、その回避策として「クロージャへの引数渡し」と「変数シャドーイング」の2つのイディオムを提示しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d38ed2a9f224dbc79adbed4fe49fe2aef137bf5b
元コミット内容
doc/effective_go: fix server example that shares var between goroutines
Use it as a teaching example about how to solve this problem.
Fixes #6501
R=golang-dev, adg, rsc
CC=golang-dev
https://golang.org/cl/14250043
変更の背景
Go言語における並行処理は、ゴルーチンとチャネルによって非常に強力かつ簡潔に記述できます。しかし、その簡潔さゆえに、特定のパターンで意図しないバグが発生することがあります。その典型的な例が、for
ループ内でゴルーチンを起動し、ループ変数をゴルーチン内のクロージャがキャプチャする場合です。
元の effective_go.html
のサーバー例では、リクエストを処理するために for
ループ内でゴルーチンを起動していました。この際、ループ変数 req
をゴルーチン内のクロージャが直接参照していました。Goの for
ループでは、ループ変数は各イテレーションで再利用されるため、すべてのゴルーチンが同じ req
変数を参照してしまいます。これにより、ゴルーチンが実際に実行される頃には req
の値がループの最終イテレーションの値になってしまったり、複数のゴルーチンが同時に同じ req
を変更しようとして競合状態に陥ったりする可能性がありました。
このコミットは、この一般的な落とし穴を明確に指摘し、Goプログラマーがこの問題を理解し、正しく解決するための教育的な例として effective_go.html
を更新することを目的としています。
前提知識の解説
ゴルーチン (Goroutines)
ゴルーチンはGoランタイムによって管理される軽量なスレッドです。go
キーワードを使って関数呼び出しの前に置くことで、その関数を新しいゴルーチンとして並行して実行させることができます。ゴルーチンは非常に軽量であり、数千、数万ものゴルーチンを同時に実行することが可能です。
クロージャ (Closures)
クロージャは、それが定義された環境(レキシカルスコープ)の変数を「キャプチャ」する関数です。Goでは、無名関数(匿名関数)がクロージャとして振る舞うことがよくあります。クロージャが変数をキャプチャする場合、その変数の「参照」をキャプチャするため、元の変数の値が変更されると、クロージャ内のその変数の値も変更されます。
for
ループにおける変数とゴルーチン
Goの for
ループでは、ループ変数は各イテレーションで新しいメモリ領域を割り当てるのではなく、既存の変数を再利用します。例えば、for i := 0; i < 5; i++
のようなループでは、i
はループ全体で同じメモリ位置を指し続けます。
この特性とクロージャの変数キャプチャが組み合わさると、以下のような問題が発生します。
package main
import (
"fmt"
"time"
)
func main() {
values := []int{1, 2, 3}
for _, v := range values {
go func() {
// ここで v を参照
fmt.Println(v)
}()
}
time.Sleep(time.Second) // ゴルーチンが実行されるのを待つ
}
このコードを実行すると、期待される 1, 2, 3
ではなく、3, 3, 3
のように同じ値が複数回出力されることがあります。これは、ゴルーチンが起動され、実際に fmt.Println(v)
が実行される頃には、ループがすでに終了しており、v
がループの最終イテレーションの値(この場合は 3
)になっているためです。すべてのゴルーチンが同じ v
変数を参照しているため、このような結果になります。
変数シャドーイング (Variable Shadowing)
Goでは、内側のスコープで外側のスコープの変数と同じ名前の新しい変数を宣言することができます。これを「変数シャドーイング」と呼びます。シャドーイングされた変数は、その内側のスコープでのみ有効であり、外側の変数を一時的に「隠蔽」します。このコミットでは、この特性をゴルーチン内のクロージャがループ変数を正しくキャプチャするために利用しています。
技術的詳細
このコミットは、for
ループ内でゴルーチンがループ変数を誤って共有する問題を解決するための2つの主要なイディオムを effective_go.html
に追加しています。
1. クロージャへの引数渡し
最も一般的で推奨される解決策の一つは、ループ変数の値をゴルーチン内のクロージャに引数として明示的に渡すことです。
func Serve(queue chan *Request) {
for req := range queue {
<-sem
go func(req *Request) { // req を引数として受け取る
process(req)
sem <- 1
}(req) // ここで現在の req の値を引数として渡す
}
}
この方法では、go func(req *Request)
の部分で新しい req
変数がクロージャのスコープ内に作成され、ループの各イテレーションで (req)
の部分で渡される現在の req
の値がその新しい変数にコピーされます。これにより、各ゴルーチンはループのそのイテレーション時点での req
の独自のコピーを持つことになり、他のゴルーチンの実行やループの進行によって値が変更される心配がなくなります。
2. 変数シャドーイングによる新しい変数の作成
もう一つの解決策は、ループの各イテレーションで、ループ変数をシャドーイングする新しいローカル変数を宣言することです。
func Serve(queue chan *Request) {
for req := range queue {
<-sem
req := req // ここで新しい req 変数を作成し、ループ変数の値をコピー
go func() {
process(req) // 新しい req を参照
sem <- 1
}()
}
}
この req := req
という記述は一見奇妙に見えますが、Goでは合法かつイディオム的な書き方です。これは、現在のスコープ(この場合は for
ループの各イテレーションのブロック)内で、外側のスコープ(for
ループ自体)の req
変数と同じ名前の新しい req
変数を宣言し、そこに外側の req
の現在の値をコピーすることを意味します。この新しい req
は、そのイテレーションのローカルスコープに限定され、ゴルーチン内のクロージャはこの新しいローカルな req
をキャプチャします。結果として、各ゴルーチンはループのそのイテレーション時点での req
の独自のコピーを持つことになります。
どちらの方法も同じ問題を解決しますが、コードの意図をより明確にするために、引数として渡す方法が好まれることが多いです。しかし、シャドーイングもGoのイディオムとして広く認識されています。
コアとなるコードの変更箇所
--- a/doc/effective_go.html
+++ b/doc/effective_go.html
@@ -2981,12 +2981,53 @@ of them can run at any moment.\n As a result, the program can consume unlimited resources if the requests come in too fast.\n We can address that deficiency by changing <code>Serve</code> to\n gate the creation of the goroutines.\n+Here\'s an obvious solution, but beware it has a bug we\'ll fix subsequently:\n </p>\n \n <pre>\n func Serve(queue chan *Request) {\n for req := range queue {\n <-sem\n+ go func() {\n+ process(req) // Buggy; see explanation below.\n+ sem <- 1\n+ }()\n+ }\n+}</pre>\n+\n+<p>\n+The bug is that in a Go <code>for</code> loop, the loop variable\n+is reused for each iteration, so the <code>req</code>\n+variable is shared across all goroutines.\n+That\'s not what we want.\n+We need to make sure that <code>req</code> is unique for each goroutine.\n+Here\'s one way to do that, passing the value of <code>req</code> as an argument\n+to the closure in the goroutine:\n+</p>\n+\n+<pre>\n+func Serve(queue chan *Request) {\n+ for req := range queue {\n+ <-sem\n+ go func(req *Request) {\n+ process(req)\n+ sem <- 1\n+ }(req)\n+ }\n+}</pre>\n+\n+<p>\n+Compare this version with the previous to see the difference in how\n+the closure is declared and run.\n+Another solution is just to create a new variable with the same\n+name, as in this example:\n+</p>\n+\n+<pre>\n+func Serve(queue chan *Request) {\n+ for req := range queue {\n+ <-sem\n+ req := req // Create new instance of req for the goroutine.\n go func() {\n process(req)\n sem <- 1\n@@ -2995,7 +3036,22 @@ func Serve(queue chan *Request) {\n }</pre>\n \n <p>\n-Another solution that manages resources well is to start a fixed\n+It may seem odd to write\n+</p>\n+\n+<pre>\n+req := req\n+</pre>\n+\n+<p>\n+but it\'s a legal and idiomatic in Go to do this.\n+You get a fresh version of the variable with the same name, deliberately\n+shadowing the loop variable locally but unique to each goroutine.\n+</p>\n+\n+<p>\n+Going back to the general problem of writing the server,\n+another approach that manages resources well is to start a fixed\n number of <code>handle</code> goroutines all reading from the request\n channel.\n The number of goroutines limits the number of simultaneous\n```
## コアとなるコードの解説
このコミットは、`doc/effective_go.html` の `Serve` 関数の例に、ゴルーチンとループ変数の共有に関する説明を追加しています。
1. **バグのある例の提示**:
```go
func Serve(queue chan *Request) {
for req := range queue {
<-sem
go func() {
process(req) // Buggy; see explanation below.
sem <- 1
}()
}
}
```
まず、`process(req)` の行に「Buggy; see explanation below.」というコメントが追加され、このコードがバグを含んでいることが明示されています。これは、`req` がループ変数であり、ゴルーチンが起動されるたびに同じ `req` 変数を参照してしまうためです。
2. **バグの説明**:
```html
<p>
The bug is that in a Go <code>for</code> loop, the loop variable
is reused for each iteration, so the <code>req</code>
variable is shared across all goroutines.
That's not what we want.
We need to make sure that <code>req</code> is unique for each goroutine.
```
このHTMLの段落で、Goの `for` ループのループ変数が各イテレーションで再利用されるため、`req` 変数がすべてのゴルーチン間で共有されてしまうというバグの根本原因が明確に説明されています。そして、各ゴルーチンに対して `req` がユニークである必要があることが述べられています。
3. **解決策1: クロージャへの引数渡し**:
```go
func Serve(queue chan *Request) {
for req := range queue {
<-sem
go func(req *Request) { // req を引数として受け取る
process(req)
sem <- 1
}(req) // ここで現在の req の値を引数として渡す
}
}
```
このコードブロックでは、無名関数(クロージャ)が `req *Request` という引数を受け取るように変更され、ゴルーチンを起動する際に `(req)` として現在の `req` の値が明示的に渡されています。これにより、各ゴルーチンはループのそのイテレーション時点での `req` の値の独自のコピーを持つことになります。
4. **解決策2: 変数シャドーイング**:
```go
func Serve(queue chan *Request) {
for req := range queue {
<-sem
req := req // Create new instance of req for the goroutine.
go func() {
process(req)
sem <- 1
}()
}
}
```
このコードブロックでは、`req := req` という行が追加されています。これは、ループの各イテレーションで、外側のループ変数 `req` の値をコピーして、そのイテレーションのローカルスコープに新しい `req` 変数を作成する「変数シャドーイング」のGoイディオムを示しています。ゴルーチン内のクロージャはこの新しいローカルな `req` をキャプチャするため、各ゴルーチンは独自の `req` のコピーを持つことになります。
5. **シャドーイングの説明**:
```html
<p>
It may seem odd to write
</p>
<pre>
req := req
</pre>
<p>
but it's a legal and idiomatic in Go to do this.
You get a fresh version of the variable with the same name, deliberately
shadowing the loop variable locally but unique to each goroutine.
</p>
```
`req := req` という書き方がGoにおいて合法かつイディオム的であること、そしてそれがどのようにして各ゴルーチンにユニークな変数のコピーを提供するのかが説明されています。
これらの変更により、`effective_go.html` はGoの並行処理における一般的な落とし穴とその効果的な解決策を、具体的なコード例と詳細な説明で提供する、より教育的なリソースとなっています。
## 関連リンク
* GitHubコミットページ: [https://github.com/golang/go/commit/d38ed2a9f224dbc79adbed4fe49fe2aef137bf5b](https://github.com/golang/go/commit/d38ed2a9f224dbc79adbed4fe49fe2aef137bf5b)
* Go言語公式ドキュメント `Effective Go`: このコミットが変更を加えたドキュメントの全体像を理解するために参照すると良いでしょう。最新版は [https://go.dev/doc/effective_go](https://go.dev/doc/effective_go) で確認できます。
## 参考にした情報源リンク
* Go言語公式ドキュメント `Effective Go` (コミット対象ファイル)
* Go言語のクロージャとループ変数のキャプチャに関する一般的な知識 (Goコミュニティの慣習とベストプラクティス)
* Go言語の変数シャドーイングに関する一般的な知識
* コミットメッセージに記載されている `Fixes #6501` については、公開されているGoリポジトリのIssueトラッカーでは直接関連するIssueを見つけることができませんでした。これは内部的なトラッキング番号であるか、または非常に古いIssueである可能性があります。