[インデックス 1854] ファイルの概要
コミット
range
キーワードがチャネルに対応し、チャネルからの値の受信をループで処理できるようになりました。また、配列に対するrange
ループにおいて、配列を生成する式が複数回評価されるバグが修正されました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/54aa835b44cf62fa503edb174f507a6331da8b7a
元コミット内容
range over channels.
also fix multiple-evaluation bug in range over arrays.
R=ken
OCL=26576
CL=26576
変更の背景
Go言語の初期段階において、for ... range
構文は配列、スライス、マップに対してのみ利用可能でした。しかし、Goの並行処理モデルの根幹をなすチャネルに対しても、同様のイテレーションメカニズムが求められていました。チャネルからの値の受信を簡潔かつ安全にループ処理できる機能は、Goの並行プログラミングの表現力を大きく向上させます。
また、既存の配列に対するfor ... range
ループには、ループの対象となる配列(またはスライス)を生成する式が、ループの各イテレーションで複数回評価されてしまうという潜在的なバグが存在していました。これは、特に配列生成に副作用を伴う関数呼び出しが含まれる場合に、予期せぬ動作を引き起こす可能性がありました。このコミットは、これらの課題に対処するために行われました。
前提知識の解説
Go言語のfor ... range
構文
Go言語のfor ... range
構文は、コレクション(配列、スライス、マップ)や文字列の要素をイテレートするための簡潔な方法を提供します。
基本的な形式は以下の通りです。
for index, value := range collection {
// indexとvalueを使った処理
}
- 配列/スライス:
index
は要素のインデックス、value
はそのインデックスの要素です。 - マップ:
index
はキー、value
は値です。 - 文字列:
index
はUnicodeコードポイントの開始バイトオフセット、value
はルーン(Unicodeコードポイント)です。
Go言語のチャネル
チャネルはGoにおけるゴルーチン間の通信手段です。チャネルを通じて値を送受信することで、ゴルーチンは安全にデータを共有し、同期を取ることができます。
make(chan Type)
:Type
型の値を送受信できるチャネルを作成します。ch <- value
:value
をチャネルch
に送信します。value := <-ch
: チャネルch
から値を受信し、value
に代入します。close(ch)
: チャネルを閉じます。閉じられたチャネルからの受信は、チャネルにまだ値が残っていればそれらを受け取り、その後はゼロ値とfalse
(チャネルが閉じられたことを示す)を返します。
Goコンパイラの内部構造(src/cmd/gc/walk.c
の役割)
src/cmd/gc/walk.c
はGoコンパイラのバックエンドの一部であり、抽象構文木(AST)を走査(walk)し、より低レベルの中間表現に変換する役割を担っています。このファイルには、Go言語の各構文要素(例えばfor
ループやrange
式)がどのようにコンパイルされるかを定義する関数が含まれています。dorange
関数は、for ... range
構文のコンパイルロジックを処理します。
技術的詳細
チャネルに対するrange
の導入
このコミットにより、for ... range
構文がチャネル型(TCHAN
)をサポートするようになりました。チャネルに対するrange
ループは、チャネルが閉じられるまで、チャネルから値を受信し続けます。
構文は以下のようになります。
for value := range channel {
// valueを使った処理
}
重要な点として、チャネルに対するrange
では、インデックス変数(k
)は許可されず、値変数(v
)のみが許可されます。これは、チャネルからの受信は順序付けられたインデックスを持たないためです。
コンパイラ内部(src/cmd/gc/walk.c
のdorange
関数)では、チャネルに対するrange
ループは以下のように変換されます。
- 隠しチャネル変数 (
hc
) の導入:range
の対象となるチャネルは、ループの開始時に一度だけ評価され、隠し変数hc
に格納されます。 - 隠し値変数 (
hv
) の導入: チャネルから受信した値を一時的に保持するための隠し変数hv
が導入されます。 - ループの初期化 (
ninit
):hc
に元のチャネルを代入します。hv
にhc
からの最初の受信値を代入します。
- ループのテスト条件 (
ntest
):!closed(hc)
という条件が設定されます。これは、チャネルhc
が閉じられていない限りループを続行することを意味します。closed
はチャネルが閉じられているかどうかをチェックする組み込み関数です。
- ループのインクリメント (
nincr
):hv
にhc
からの次の受信値を代入します。これにより、各イテレーションの開始時に新しい値がhv
にロードされます。
- ループ本体 (
nbody
):hv
の値をユーザーが指定したループ変数(例:value
)に代入します。
この変換により、チャネルからの値の受信、チャネルのクローズ状態のチェック、そしてループ変数の更新が、コンパイラによって自動的に処理され、開発者は簡潔なfor ... range
構文でチャネルをイテレートできるようになります。
配列に対するrange
の複数評価バグの修正
以前のGoコンパイラでは、配列(またはスライス)に対するfor ... range
ループにおいて、range
キーワードの後に続く配列を生成する式が、ループの各イテレーションで評価されてしまうバグがありました。例えば、for k, v := range makearray() { ... }
のようなコードでは、makearray()
関数がループの各イテレーションで呼び出されてしまう可能性がありました。
このコミットでは、このバグを修正するために、配列に対するrange
ループのコンパイル方法が変更されました。
- 隠し配列変数 (
ha
) の導入:range
の対象となる配列(またはスライス)は、ループの開始時に一度だけ評価され、隠し変数ha
に格納されます。 - ループの初期化 (
ninit
) の変更:ha
に元の配列を代入する処理が追加されます。
- ループのテスト条件 (
ntest
) と要素アクセス (OINDEX
) の変更:len(ha)
やha[hk]
のように、以降のループ処理ではすべてこの隠し変数ha
が参照されるようになります。
これにより、makearray()
のような配列を生成する式はループの開始時に一度だけ評価され、その結果がha
に格納されるため、ループの各イテレーションで不必要に再評価されることがなくなりました。これは、パフォーマンスの向上と、副作用を持つ関数が予期せず複数回実行されることの防止に貢献します。
コアとなるコードの変更箇所
src/cmd/gc/walk.c
dorange
関数内で、TCHAN
(チャネル型)を処理するための新しいgoto chan;
ブロックが追加されました。chan:
ラベル以下に、チャネルからの受信を処理するための新しいロジックが実装されました。これには、隠しチャネル変数hc
と隠し値変数hv
の導入、ループの初期化、テスト条件、インクリメント、本体の構築が含まれます。ary:
ラベル以下で、配列に対するrange
の複数評価バグを修正するために、隠し配列変数ha
が導入され、元の配列m
がha
に一度だけ代入されるように変更されました。これにより、OLEN
やOINDEX
操作がha
を参照するようになりました。
test/chan/sieve.go
Filter
関数内のfor
ループが、for { i := <-in; ... }
から新しいチャネルrange
構文for i := range in { ... }
に変更されました。これは、新しい機能の実際の使用例を示しています。
test/range.go
- 新しいテストファイルが追加されました。
testchan()
関数は、チャネルに対するrange
ループが正しく動作することを確認します。testarray()
関数は、配列に対するrange
ループが、配列を生成する式を一度だけ評価することを確認します。makearray()
関数が一度だけ呼び出されることをnmake
カウンタで検証しています。
コアとなるコードの解説
src/cmd/gc/walk.c
におけるdorange
関数の変更点
チャネルに対するrange
の処理 (chan:
ブロック)
+chan:
+ if(v != N)
+ yyerror("chan range can only have one variable");
+
+ hc = nod(OXXX, N, N); // hidden chan
+ tempname(hc, t);
+
+ hv = nod(OXXX, N, N); // hidden value
+ tempname(hv, t->type);
+
+ n->ninit = list(
+ nod(OAS, hc, m),
+ nod(OAS, hv, nod(ORECV, hc, N))
+ );
+ n->ntest = nod(ONOT, nod(OCLOSED, hc, N), N);
+ n->nincr = nod(OAS, hv, nod(ORECV, hc, N));
+
+ if(local)
+ k = old2new(k, hv->type);
+ n->nbody = nod(OAS, k, hv);
+ addtotop(n);
+ goto out;
if(v != N)
:range
ループで2つ目の変数(値変数)が指定されている場合(例:for k, v := range ch
)、エラーを報告します。チャネルのrange
は1つの変数(受信値)のみをサポートするためです。hc = nod(OXXX, N, N); tempname(hc, t);
: 隠しチャネル変数hc
を宣言し、元のチャネルt
と同じ型を設定します。hv = nod(OXXX, N, N); tempname(hv, t->type);
: 隠し値変数hv
を宣言し、チャネルの要素型(t->type
)を設定します。n->ninit = list(...)
: ループの初期化部分を構築します。nod(OAS, hc, m)
: 元のチャネル式m
の評価結果をhc
に代入します。これにより、チャネル式が一度だけ評価されます。nod(OAS, hv, nod(ORECV, hc, N))
:hc
から最初の値を受信し、hv
に代入します。ORECV
はチャネル受信操作を表すノードです。
n->ntest = nod(ONOT, nod(OCLOSED, hc, N), N);
: ループのテスト条件を構築します。OCLOSED
はチャネルが閉じられているかをチェックする操作を表し、ONOT
はその結果を反転させます。つまり、チャネルが閉じられていない間はループを続行します。n->nincr = nod(OAS, hv, nod(ORECV, hc, N));
: ループのインクリメント部分を構築します。各イテレーションの終わりに、hc
から次の値を受信し、hv
に代入します。n->nbody = nod(OAS, k, hv);
: ループ本体を構築します。hv
に格納された受信値を、ユーザーが指定したループ変数k
(この場合は値変数)に代入します。
配列に対するrange
の複数評価バグ修正 (ary:
ブロック)
ary:
hk = nod(OXXX, N, N); // hidden key
tempname(hk, types[TINT]);
+ ha = nod(OXXX, N, N); // hidden array
+ tempname(ha, t);
+
n->ninit = nod(OAS, hk, nodintconst(0));
- n->ntest = nod(OLT, hk, nod(OLEN, m, N));
+ n->ninit = list(nod(OAS, ha, m), n->ninit);
+
+ n->ntest = nod(OLT, hk, nod(OLEN, ha, N));
n->nincr = nod(OASOP, hk, nodintconst(1));
n->nincr->etype = OADD;
if(v != N) {
if(local)
v = old2new(v, t->type);
n->nbody = list(n->nbody,
- nod(OAS, v, nod(OINDEX, m, hk)) );
+ nod(OAS, v, nod(OINDEX, ha, hk)) );
}
addtotop(n);
goto out;
ha = nod(OXXX, N, N); tempname(ha, t);
: 隠し配列変数ha
を宣言し、元の配列t
と同じ型を設定します。n->ninit = list(nod(OAS, ha, m), n->ninit);
: 既存の初期化リストの先頭に、元の配列式m
の評価結果をha
に代入する処理を追加します。これにより、m
はループ開始時に一度だけ評価されます。n->ntest = nod(OLT, hk, nod(OLEN, ha, N));
: ループのテスト条件で、配列の長さを取得する際にm
の代わりにha
を参照するように変更します。OLEN
は長さ取得操作を表します。nod(OAS, v, nod(OINDEX, ha, hk))
: ループ本体で配列要素にアクセスする際に、m
の代わりにha
を参照するように変更します。OINDEX
はインデックスアクセス操作を表します。
これらの変更により、Goコンパイラはfor ... range
構文をより強力かつ堅牢に処理できるようになりました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語の
for
ステートメントに関するドキュメント: https://go.dev/ref/spec#For_statements - Go言語のチャネルに関するドキュメント: https://go.dev/ref/spec#Channel_types
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/walk.c
): https://github.com/golang/go/blob/master/src/cmd/gc/walk.c - Go言語のテストコード (特に
test/chan/sieve.go
とtest/range.go
):test/chan/sieve.go
: https://github.com/golang/go/blob/master/test/chan/sieve.gotest/range.go
: https://github.com/golang/go/blob/master/test/range.go