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

[インデックス 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.cdorange関数)では、チャネルに対するrangeループは以下のように変換されます。

  1. 隠しチャネル変数 (hc) の導入: rangeの対象となるチャネルは、ループの開始時に一度だけ評価され、隠し変数hcに格納されます。
  2. 隠し値変数 (hv) の導入: チャネルから受信した値を一時的に保持するための隠し変数hvが導入されます。
  3. ループの初期化 (ninit):
    • hcに元のチャネルを代入します。
    • hvhcからの最初の受信値を代入します。
  4. ループのテスト条件 (ntest):
    • !closed(hc)という条件が設定されます。これは、チャネルhcが閉じられていない限りループを続行することを意味します。closedはチャネルが閉じられているかどうかをチェックする組み込み関数です。
  5. ループのインクリメント (nincr):
    • hvhcからの次の受信値を代入します。これにより、各イテレーションの開始時に新しい値がhvにロードされます。
  6. ループ本体 (nbody):
    • hvの値をユーザーが指定したループ変数(例: value)に代入します。

この変換により、チャネルからの値の受信、チャネルのクローズ状態のチェック、そしてループ変数の更新が、コンパイラによって自動的に処理され、開発者は簡潔なfor ... range構文でチャネルをイテレートできるようになります。

配列に対するrangeの複数評価バグの修正

以前のGoコンパイラでは、配列(またはスライス)に対するfor ... rangeループにおいて、rangeキーワードの後に続く配列を生成する式が、ループの各イテレーションで評価されてしまうバグがありました。例えば、for k, v := range makearray() { ... }のようなコードでは、makearray()関数がループの各イテレーションで呼び出されてしまう可能性がありました。

このコミットでは、このバグを修正するために、配列に対するrangeループのコンパイル方法が変更されました。

  1. 隠し配列変数 (ha) の導入: rangeの対象となる配列(またはスライス)は、ループの開始時に一度だけ評価され、隠し変数haに格納されます。
  2. ループの初期化 (ninit) の変更:
    • haに元の配列を代入する処理が追加されます。
  3. ループのテスト条件 (ntest) と要素アクセス (OINDEX) の変更:
    • len(ha)ha[hk]のように、以降のループ処理ではすべてこの隠し変数haが参照されるようになります。

これにより、makearray()のような配列を生成する式はループの開始時に一度だけ評価され、その結果がhaに格納されるため、ループの各イテレーションで不必要に再評価されることがなくなりました。これは、パフォーマンスの向上と、副作用を持つ関数が予期せず複数回実行されることの防止に貢献します。

コアとなるコードの変更箇所

src/cmd/gc/walk.c

  • dorange関数内で、TCHAN(チャネル型)を処理するための新しいgoto chan;ブロックが追加されました。
  • chan:ラベル以下に、チャネルからの受信を処理するための新しいロジックが実装されました。これには、隠しチャネル変数hcと隠し値変数hvの導入、ループの初期化、テスト条件、インクリメント、本体の構築が含まれます。
  • ary:ラベル以下で、配列に対するrangeの複数評価バグを修正するために、隠し配列変数haが導入され、元の配列mhaに一度だけ代入されるように変更されました。これにより、OLENOINDEX操作が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構文をより強力かつ堅牢に処理できるようになりました。

関連リンク

参考にした情報源リンク