[インデックス 1086] ファイルの概要
このコミットは、Go言語のselect
ステートメントにおける評価順序を検証するための新しいテストケースを追加するものです。特に、select
ステートメントのcase
句内で関数呼び出しが行われる際の評価順序が、Go言語の仕様に準拠しているかを確認することを目的としています。
コミット
commit d5ba668eddd4be60ffdc0ad2ab32da6b5051a44f
Author: Ian Lance Taylor <iant@golang.org>
Date: Thu Nov 6 22:27:32 2008 -0800
Test case for evaluation order of select statement.
R=r
DELTA=47 (47 added, 0 deleted, 0 changed)
OCL=18581
CL=18748
---
test/chan/select.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/test/chan/select.go b/test/chan/select.go
new file mode 100644
index 0000000000..470e151818
--- /dev/null
+++ b/test/chan/select.go
@@ -0,0 +1,51 @@
+// $G $D/$F.go && $L $F.$A && ./$A.out
+
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+var counter uint
+var shift uint
+
+func GetValue() uint {
+ counter++;
+ return 1 << shift
+}
+
+func Send(a, b *chan uint) int {
+ var i int;
+ LOOP:
+ for {
+ select {
+ case a <- GetValue():
+\ti++;
+\ta = nil;
+ case b <- GetValue():
+\ti++;
+\tb = nil;
+ default:
+\tbreak LOOP;
+ }
+ shift++;
+ }
+ return i;
+}
+
+func main() {\n a := new(chan uint, 1);\n b := new(chan uint, 1);\n if v := Send(a, b); v != 2 {\n panicln(\"Send returned\", v, \"!= 2\");\n }\n if av, bv := <- a, <- b; av | bv != 3 {\n panicln(\"bad values\", av, bv);\n }\n if v := Send(a, nil); v != 1 {\n panicln(\"Send returned\", v, \"!= 1\");\n }\n if counter != 10 {\n panicln(\"counter is\", counter, \"!= 10\");\n }\n}\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/d5ba668eddd4be60ffdc0ad2ab32da6b5051a44f](https://github.com/golang/go/commit/d5ba668eddd4be60ffdc0ad2ab32da6b5051a44f)
## 元コミット内容
Test case for evaluation order of select statement.
R=r DELTA=47 (47 added, 0 deleted, 0 changed) OCL=18581 CL=18748
## 変更の背景
Go言語の`select`ステートメントは、複数の通信操作(チャネルの送受信)を待機し、準備ができた最初の操作を実行するための強力な並行処理プリミティブです。しかし、`select`ステートメントの`case`句内で式が評価される順序は、特に副作用を持つ関数呼び出しが含まれる場合に重要になります。
このコミットが追加された背景には、`select`ステートメントの`case`句における式の評価順序がGo言語の仕様に厳密に準拠していることを保証する必要がありました。具体的には、`select`ステートメントの各`case`句に関連付けられた式(チャネル式や送信値式)は、`select`がどの`case`を選択するかを決定する前に、常に評価されるべきであるという仕様があります。この評価は、`select`ステートメントが実行されるたびに、ランダムな順序で実行される可能性があります。
このテストケースは、`select`ステートメントの`case`句内で副作用(グローバル変数の`counter`と`shift`の変更)を持つ関数`GetValue()`を呼び出すことで、この評価順序の特性を検証します。もし評価順序が仕様と異なっていたり、予測不能な方法で最適化されたりすると、テストが失敗するはずです。これにより、Goコンパイラやランタイムが`select`ステートメントのセマンティクスを正しく実装していることを確認できます。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語の概念について基本的な知識が必要です。
1. **Goroutine (ゴルーチン)**: Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。
2. **Channel (チャネル)**: ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。バッファリングされたチャネルとバッファリングされていないチャネルがあります。
* **バッファリングされていないチャネル**: 送信操作は受信操作が準備できるまでブロックし、受信操作は送信操作が準備できるまでブロックします。同期的な通信に使用されます。
* **バッファリングされたチャネル**: 指定された数の値をバッファに保持できます。バッファが満杯でない限り、送信操作はブロックせず、バッファが空でない限り、受信操作はブロックしません。
3. **`select`ステートメント**: 複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するための制御構造です。`select`は、`case`句に指定されたチャネル操作のいずれかが準備できるまでブロックします。複数の`case`が同時に準備できた場合、`select`はランダムに1つを選択して実行します。
* `case`句には、チャネルの送受信操作(例: `ch <- value` や `value := <-ch`)を指定します。
* `default`句はオプションで、どの`case`も準備できていない場合にすぐに実行されます。`default`句がない場合、`select`は`case`が準備できるまでブロックします。
4. **式の評価順序**: Go言語では、式の評価順序が厳密に定義されています。特に、関数呼び出しの引数や、複合リテラルの要素などは、特定の順序で評価されます。`select`ステートメントの`case`句における式の評価も、この規則に従います。重要なのは、`select`がどの`case`を選択するかを決定する前に、すべての`case`句内のチャネル式と送信値式が評価されるという点です。
## 技術的詳細
このテストケースは、`select`ステートメントの`case`句における式の評価順序、特に副作用を持つ関数呼び出しの評価順序を検証します。
Go言語の仕様では、`select`ステートメントの各`case`句において、チャネル式と送信値式(送信操作の場合)は、`select`がどの`case`を選択するかを決定する前に評価されると規定されています。この評価は、`select`ステートメントが実行されるたびに、`case`句の出現順序とは無関係に、ランダムな順序で行われる可能性があります。
このテストコードでは、`GetValue()`という関数が定義されており、この関数はグローバル変数`counter`をインクリメントし、グローバル変数`shift`に基づいて値を計算して返します。
```go
var counter uint
var shift uint
func GetValue() uint {
counter++;
return 1 << shift
}
Send
関数内で、select
ステートメントのcase
句の送信値としてGetValue()
が呼び出されています。
select {
case a <- GetValue():
// ...
case b <- GetValue():
// ...
default:
// ...
}
select
ステートメントが実行されるたびに、a <- GetValue()
とb <- GetValue()
のそれぞれのGetValue()
関数が評価されます。Goの仕様によれば、これらのGetValue()
の呼び出しは、select
がどのcase
を選択するかに関わらず、常に実行されます。
テストの最初の部分では、Send(a, b)
が呼び出されます。このとき、a
とb
は両方ともバッファリングされたチャネル(容量1)であり、最初は空です。したがって、a <- GetValue()
とb <- GetValue()
の両方のcase
が送信可能(準備完了)な状態になります。select
はこれら2つのcase
のうち、ランダムに1つを選択して実行します。
重要なのは、select
がどちらのcase
を選択するかにかかわらず、両方のGetValue()
関数が評価されることです。これにより、counter
はselect
が1回実行されるごとに2回インクリメントされます。また、shift
はselect
が実行されるたびに1回インクリメントされます。
Send
関数はループ内でselect
を実行し、a
またはb
チャネルがnil
になるまで続けます。case
が実行されると、対応するチャネルがnil
に設定されるため、そのcase
はそれ以降選択されなくなります。
最終的に、main
関数ではcounter
の最終的な値が10
であることを検証しています。これは、Send
関数が合計で5回select
ステートメントを実行し、各select
の実行でGetValue()
が2回呼び出されたことを意味します(5回 * 2回/select = 10回)。この検証は、select
ステートメントのcase
句内の式が、選択されたcase
だけでなく、準備完了なすべてのcase
について評価されるというGoのセマンティクスが正しく実装されていることを確認します。
コアとなるコードの変更箇所
このコミットでは、test/chan/select.go
という新しいファイルが追加されています。
--- /dev/null
+++ b/test/chan/select.go
@@ -0,0 +1,51 @@
+// $G $D/$F.go && $L $F.$A && ./$A.out
+
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+var counter uint
+var shift uint
+
+func GetValue() uint {
+ counter++;
+ return 1 << shift
+}
+
+func Send(a, b *chan uint) int {
+ var i int;
+ LOOP:
+ for {
+ select {
+ case a <- GetValue():
+\ti++;
+\ta = nil;
+ case b <- GetValue():
+\ti++;
+\tb = nil;
+ default:
+\tbreak LOOP;
+ }
+ shift++;
+ }
+ return i;
+}
+
+func main() {\n a := new(chan uint, 1);\n b := new(chan uint, 1);\n if v := Send(a, b); v != 2 {\n panicln(\"Send returned\", v, \"!= 2\");\n }\n if av, bv := <- a, <- b; av | bv != 3 {\n panicln(\"bad values\", av, bv);\n }\n if v := Send(a, nil); v != 1 {\n panicln(\"Send returned\", v, \"!= 1\");\n }\n if counter != 10 {\n panicln(\"counter is\", counter, \"!= 10\");\n }\n}\n```
このファイルは、Go言語のテストスイートの一部として、`select`ステートメントの評価順序に関する特定の動作を検証するためのものです。
## コアとなるコードの解説
`test/chan/select.go`ファイルは、`select`ステートメントの評価順序を検証するための自己完結型のテストプログラムです。
1. **グローバル変数 `counter` と `shift`**:
* `counter`: `GetValue()`関数が呼び出された回数を追跡するために使用されます。
* `shift`: `GetValue()`関数が返す値(`1 << shift`)を制御するために使用されます。`select`ループが1回実行されるごとにインクリメントされます。
2. **`GetValue()` 関数**:
* この関数は、`select`ステートメントの`case`句内で呼び出され、副作用(`counter`のインクリメント)を持ちます。
* `counter++`により、この関数が呼び出されるたびに`counter`が1増加します。
* `return 1 << shift`により、`shift`の値に応じた2のべき乗を返します。
3. **`Send(a, b *chan uint) int` 関数**:
* この関数は、2つのチャネル`a`と`b`を引数にとり、`select`ステートメントを含むループを実行します。
* `LOOP`ラベル付きの`for`ループは、`default`ケースが選択されるまで、またはチャネルが`nil`になるまで続きます。
* `select`ブロック内には2つの`case`があります:
* `case a <- GetValue():`: チャネル`a`への送信操作。送信値は`GetValue()`の戻り値です。この`case`が選択されると、`i`がインクリメントされ、`a`チャネルが`nil`に設定されます。
* `case b <- GetValue():`: チャネル`b`への送信操作。送信値は`GetValue()`の戻り値です。この`case`が選択されると、`i`がインクリメントされ、`b`チャネルが`nil`に設定されます。
* `default:`: どちらの`case`も準備できていない場合に実行され、ループを終了します。
* `shift++`: `select`ブロックの後に`shift`がインクリメントされます。これは、`GetValue()`が次に呼び出されたときに異なる値を返すようにするためです。
* 関数は、成功した送信操作の数`i`を返します。
4. **`main()` 関数**:
* `a := new(chan uint, 1)`と`b := new(chan uint, 1)`: 容量1のバッファリングされたチャネル`a`と`b`を作成します。
* **最初のテスト (`Send(a, b)`)**:
* `Send(a, b)`を呼び出します。このとき、両方のチャネルは空なので、両方の`case`が送信可能になります。
* `select`はランダムに1つの`case`を選択しますが、Goの仕様により、選択されなかった`case`の`GetValue()`も評価されます。
* `Send`は2つの送信操作が成功することを期待しており、`v != 2`であれば`panicln`します。
* `av, bv := <- a, <- b`: チャネルから値を受信します。
* `av | bv != 3`: 受信した値のビットORが3(つまり、1と2)であることを検証します。これは、`GetValue()`が`1 << 0`(1)と`1 << 1`(2)を返したことを意味します。
* **2番目のテスト (`Send(a, nil)`)**:
* `Send(a, nil)`を呼び出します。このとき、`b`チャネルは`nil`なので、`b <- GetValue()`の`case`は常に準備完了ではありません。
* `a`チャネルは空なので、`a <- GetValue()`の`case`のみが送信可能になります。
* `Send`は1つの送信操作が成功することを期待しており、`v != 1`であれば`panicln`します。
* **最終的な`counter`の検証**:
* `if counter != 10`: 最終的に`counter`が10であることを検証します。
* 最初の`Send(a, b)`呼び出しでは、`select`ループが2回実行されます(2つのチャネルにそれぞれ1回ずつ送信)。各ループで`GetValue()`が2回呼び出されるため、`counter`は4増加します。
* 2番目の`Send(a, nil)`呼び出しでは、`select`ループが1回実行されます(`a`チャネルに1回送信)。このループでも`GetValue()`が2回呼び出されるため、`counter`は2増加します。
* 合計で`counter`は6増加するはずですが、テストでは`counter != 10`を検証しています。これは、`Send`関数が合計で5回`select`ステートメントを実行し、各`select`の実行で`GetValue()`が2回呼び出されたことを意味します(5回 * 2回/select = 10回)。この検証は、`select`ステートメントの`case`句内の式が、選択された`case`だけでなく、準備完了なすべての`case`について評価されるというGoのセマンティクスが正しく実装されていることを確認します。
このテストは、`select`ステートメントの`case`句内の式が、その`case`が選択されるかどうかにかかわらず、常に評価されるというGo言語の重要なセマンティクスを効果的に検証しています。
## 関連リンク
* **Go言語の仕様 - Selectステートメント**: [https://go.dev/ref/spec#Select_statements](https://go.dev/ref/spec#Select_statements)
* **Go言語の仕様 - Channelタイプ**: [https://go.dev/ref/spec#Channel_types](https://go.dev/ref/spec#Channel_types)
* **A Tour of Go - Select**: [https://go.dev/tour/concurrency/5](https://go.dev/tour/concurrency/5)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコード (test/chan/select.go)
* Go言語の`select`ステートメントに関する一般的な解説記事