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

[インデックス 19728] ファイルの概要

このコミットは、Go言語のfor...rangeステートメントにおいて、イテレーション変数を明示的に指定せずにfor range xという形式を許可するように変更を加えるものです。これにより、レンジ式の値のみを処理し、キーや値が不要な場合にコードをより簡潔に記述できるようになります。

コミット

  • コミットハッシュ: d3a2f5870034db2d69bd0ef85f18a87f4163c770
  • 作者: Robert Griesemer gri@golang.org
  • 日付: Mon Jul 14 16:17:17 2014 -0700

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d3a2f5870034db2d69bd0ef85f18a87f4163c770

元コミット内容

go/*: permit "for range x"

LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/112970044

変更の背景

Go言語のfor...rangeループは、スライス、配列、文字列、マップ、チャネルなどのコレクションをイテレートするための強力な構文です。このループは通常、for key, value := range collection {}のように、イテレーションごとにキーと値(またはインデックスと値)を受け取る2つの変数を宣言します。しかし、場合によっては、イテレーション対象のコレクションの要素数だけループを回したいだけで、キーや値自体には興味がないことがあります。

このコミット以前は、キーや値が不要な場合でも、for _, value := range collection {}のようにアンダースコア_を使って不要な変数を明示的に破棄する必要がありました。これは冗長であり、特に値も不要な場合はfor _, _ = range collection {}とさらに冗長になります。

この変更の背景には、コードの簡潔性と可読性の向上というGo言語の設計哲学があります。for range xという構文を許可することで、イテレーション対象のコレクションの要素数だけループを回したいという意図をより明確かつ簡潔に表現できるようになります。これは、例えばスライスや配列の要素を単に消費するだけで、その値自体にはアクセスしないようなシナリオで特に有用です。

前提知識の解説

このコミットの変更を理解するためには、以下のGo言語の概念と、Goコンパイラが内部的に使用するツール群に関する知識が必要です。

Go言語のfor...rangeステートメント

for...rangeステートメントは、Go言語でコレクション(スライス、配列、文字列、マップ、チャネル)をイテレートするための構文です。基本的な形式は以下の通りです。

  • スライス/配列/文字列: for index, value := range collection {}
    • indexは要素のインデックス、valueは要素の値です。
  • マップ: for key, value := range map {}
    • keyはマップのキー、valueはマップの値です。
  • チャネル: for value := range channel {}
    • valueはチャネルから受信した値です。チャネルの場合、キーは存在しません。

このコミット以前は、キーや値が不要な場合でも、以下のように明示的に変数を宣言する必要がありました。

  • キーが不要な場合: for _, value := range collection {}
  • 値が不要な場合: for key, _ := range collection {}
  • 両方不要な場合: for _, _ = range collection {}

GoのAST (Abstract Syntax Tree)

Goコンパイラは、ソースコードを解析する際に、その構造を抽象構文木(AST)として表現します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラがコードの意味を理解し、最適化やコード生成を行うための基盤となります。

  • go/astパッケージ: Go言語のASTを定義するパッケージです。ast.RangeStmtのような構造体は、for...rangeステートメントのASTノードを表現します。
  • ast.RangeStmt: for...rangeステートメントを表すASTノードの構造体です。この構造体には、For (forキーワードの位置), Key (キー変数), Value (値変数), TokPos (代入演算子の位置), Tok (代入演算子の種類), X (レンジ式の値), Body (ループ本体) などのフィールドが含まれます。
  • ast.Expr: 任意の式を表すインターフェースです。KeyValueフィールドはast.Expr型です。

go/parserパッケージ

go/parserパッケージは、Go言語のソースコードを解析し、ASTを生成するためのパッケージです。このパッケージは、ソースコードの字句解析(トークン化)と構文解析を行い、astパッケージで定義されたASTノードのインスタンスを構築します。

go/printerパッケージ

go/printerパッケージは、ASTをGo言語のソースコードとして整形して出力するためのパッケージです。これは、ASTを人間が読める形式に戻す「逆解析」の役割を担います。

tokenパッケージ

tokenパッケージは、Go言語の字句要素(トークン)とそれらの位置(token.Pos)を定義します。

  • token.Pos: ソースコード内の位置を表します。
  • token.Token: ASSIGN (代入演算子 =), DEFINE (短い変数宣言演算子 :=), ILLEGAL (不正なトークン) など、Go言語のキーワードや演算子、識別子などの種類を表します。

技術的詳細

このコミットは、for range xという新しい構文をサポートするために、Goコンパイラのフロントエンド(パーサーとプリンター)におけるASTの定義と処理ロジックを変更しています。

src/pkg/go/ast/ast.goの変更

ast.RangeStmt構造体の定義が変更されています。

--- a/src/pkg/go/ast/ast.go
+++ b/src/pkg/go/ast/ast.go
@@ -699,9 +699,9 @@ type (
 	// A RangeStmt represents a for statement with a range clause.
 	RangeStmt struct {
 		For        token.Pos   // position of "for" keyword
-		Key, Value Expr        // Value may be nil
-		TokPos     token.Pos   // position of Tok
-		Tok        token.Token // ASSIGN, DEFINE
+		Key, Value Expr        // Key, Value may be nil
+		TokPos     token.Pos   // position of Tok; invalid if Key == nil
+		Tok        token.Token // ILLEGAL if Key == nil, ASSIGN, DEFINE
 		X          Expr        // value to range over
 		Body       *BlockStmt
 	}
  • 変更前: Key, Value Expr // Value may be nil
  • 変更後: Key, Value Expr // Key, Value may be nil
    • これは、Keynilになりうることを明示しています。つまり、for range xのようにキー変数が存在しない場合、Keyフィールドがnilになることを示唆しています。
  • 変更前: TokPos token.Pos // position of Tok
  • 変更後: TokPos token.Pos // position of Tok; invalid if Key == nil
    • TokPos(代入演算子の位置)がKeynilの場合(つまりfor range xの場合)は無効になることを示しています。これは、for range x構文では代入演算子が存在しないためです。
  • 変更前: Tok token.Token // ASSIGN, DEFINE
  • 変更後: Tok token.Token // ILLEGAL if Key == nil, ASSIGN, DEFINE
    • Tok(代入演算子の種類)がKeynilの場合(for range xの場合)はtoken.ILLEGALになることを示しています。これも、代入演算子が存在しないことを反映しています。

これらの変更は、for range x構文がASTレベルでどのように表現されるかを定義しています。KeynilであるRangeStmtは、イテレーション変数が明示的に宣言されていないfor...rangeループを表すことになります。

src/pkg/go/parser/parser.goの変更

パーサーは、ソースコードを読み込み、ASTを構築する役割を担います。このコミットでは、parseForStmt関数がfor range x構文を認識し、適切にASTを構築するように変更されています。

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -2041,7 +2041,16 @@ func (p *parser) parseForStmt() ast.Stmt {
 		prevLev := p.exprLev
 		p.exprLev = -1
 		if p.tok != token.SEMICOLON {
-\t\t\ts2, isRange = p.parseSimpleStmt(rangeOk)
+\t\t\tif p.tok == token.RANGE {
+\t\t\t\t// "for range x" (nil lhs in assignment)
+\t\t\t\tpos := p.pos
+\t\t\t\tp.next()\n+\t\t\t\ty := []ast.Expr{&ast.UnaryExpr{OpPos: pos, Op: token.RANGE, X: p.parseRhs()}}\n+\t\t\t\ts2 = &ast.AssignStmt{Rhs: y}\n+\t\t\t\tisRange = true\n+\t\t\t} else {\n+\t\t\t\ts2, isRange = p.parseSimpleStmt(rangeOk)\n+\t\t\t}\
 		}
 		if !isRange && p.tok == token.SEMICOLON {
 			p.next()
@@ -2066,12 +2075,14 @@ func (p *parser) parseForStmt() ast.Stmt {
 		// check lhs
 		var key, value ast.Expr
 		switch len(as.Lhs) {
-\t\tcase 2:\
-\t\t\tkey, value = as.Lhs[0], as.Lhs[1]\
+\t\tcase 0:\
+\t\t\t// nothing to do
 		case 1:\
 			key = as.Lhs[0]
+\t\tcase 2:\
+\t\t\tkey, value = as.Lhs[0], as.Lhs[1]\
 		default:\
-\t\t\tp.errorExpected(as.Lhs[0].Pos(), "1 or 2 expressions")
+\t\t\tp.errorExpected(as.Lhs[len(as.Lhs)-1].Pos(), "at most 2 expressions")
 			return &ast.BadStmt{From: pos, To: p.safePos(body.End())}
 		}
 		// parseSimpleStmt returned a right-hand side that
  • p.tok == token.RANGEのチェックが追加され、for range xのケースを特別に処理しています。
  • この場合、左辺(LHS)がないため、ast.AssignStmtRhs(右辺)にast.UnaryExprを作成し、Optoken.RANGEXをレンジ式として設定しています。これにより、パーサーはfor range xを内部的にfor _ = range xのような形式として解釈し、ASTを構築します。
  • switch len(as.Lhs)のケースにcase 0:が追加され、左辺の式が0個の場合(for range xの場合)は何もしないようにしています。
  • エラーメッセージも"1 or 2 expressions"から"at most 2 expressions"に変更され、0個の式も許容されることを反映しています。

src/pkg/go/printer/nodes.goの変更

プリンターは、ASTをGoソースコードに変換する役割を担います。このコミットでは、RangeStmtの処理ロジックが変更され、Keynilの場合(for range xの場合)に、キーや代入演算子を出力しないようにしています。

--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -1216,14 +1216,17 @@ func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool) {
 
 	case *ast.RangeStmt:\
 		p.print(token.FOR, blank)\
-\t\tp.expr(s.Key)\
-\t\tif s.Value != nil {\
-\t\t\t// use position of value following the comma as\
-\t\t\t// comma position for correct comment placement\
-\t\t\tp.print(s.Value.Pos(), token.COMMA, blank)\
-\t\t\tp.expr(s.Value)\
-\t\t}\
-\t\tp.print(blank, s.TokPos, s.Tok, blank, token.RANGE, blank)\
+\t\tif s.Key != nil {\
+\t\t\tp.expr(s.Key)\
+\t\t\tif s.Value != nil {\
+\t\t\t\t// use position of value following the comma as\
+\t\t\t\t// comma position for correct comment placement\
+\t\t\t\tp.print(s.Value.Pos(), token.COMMA, blank)\
+\t\t\t\tp.expr(s.Value)\
+\t\t\t}\
+\t\t\tp.print(blank, s.TokPos, s.Tok, blank)\
+\t\t}\
+\t\tp.print(token.RANGE, blank)\
 		p.expr(stripParens(s.X))\
 		p.print(blank)\
 		p.block(s.Body, 1)\
  • if s.Key != nilという条件が追加され、Keyが存在する場合にのみ、キー変数と代入演算子を出力するように変更されています。
  • これにより、for range xのようにKeynilのASTノードが与えられた場合でも、プリンターは正しくfor range xという構文を生成できるようになります。

テストファイルの変更

  • src/pkg/go/parser/short_test.go: for range x {}という新しい構文を含むテストケースが追加され、パーサーがこれを正しく解析できることを確認しています。
  • src/pkg/go/printer/testdata/statements.goldensrc/pkg/go/printer/testdata/statements.input: プリンターのテストデータが更新され、for range x {}for _ = range a {}for _, _ = range a {}といった新しい構文の出力が正しく整形されることを確認しています。

これらの変更により、Go言語のツールチェーン全体でfor range x構文がサポートされるようになります。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. src/pkg/go/ast/ast.go: ast.RangeStmt構造体の定義変更。
  2. src/pkg/go/parser/parser.go: parseForStmt関数におけるfor range x構文の解析ロジックの追加。
  3. src/pkg/go/printer/nodes.go: RangeStmtの整形ロジックの変更。

src/pkg/go/ast/ast.go

type (
	// A RangeStmt represents a for statement with a range clause.
	RangeStmt struct {
		For        token.Pos   // position of "for" keyword
		Key, Value Expr        // Key, Value may be nil
		TokPos     token.Pos   // position of Tok; invalid if Key == nil
		Tok        token.Token // ILLEGAL if Key == nil, ASSIGN, DEFINE
		X          Expr        // value to range over
		Body       *BlockStmt
	}
)

src/pkg/go/parser/parser.go (抜粋)

func (p *parser) parseForStmt() ast.Stmt {
	// ... (既存のコード) ...
	if p.tok != token.SEMICOLON {
		if p.tok == token.RANGE {
			// "for range x" (nil lhs in assignment)
			pos := p.pos
			p.next()
			y := []ast.Expr{&ast.UnaryExpr{OpPos: pos, Op: token.RANGE, X: p.parseRhs()}}
			s2 = &ast.AssignStmt{Rhs: y}
			isRange = true
		} else {
			s2, isRange = p.parseSimpleStmt(rangeOk)
		}
	}
	// ... (既存のコード) ...
	switch len(as.Lhs) {
	case 0:
		// nothing to do
	case 1:
		key = as.Lhs[0]
	case 2:
		key, value = as.Lhs[0], as.Lhs[1]
	default:
		p.errorExpected(as.Lhs[len(as.Lhs)-1].Pos(), "at most 2 expressions")
		return &ast.BadStmt{From: pos, To: p.safePos(body.End())}
	}
	// ... (既存のコード) ...
}

src/pkg/go/printer/nodes.go (抜粋)

func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool) {
	// ... (既存のコード) ...
	case *ast.RangeStmt:
		p.print(token.FOR, blank)
		if s.Key != nil {
			p.expr(s.Key)
			if s.Value != nil {
				// use position of value following the comma as
				// comma position for correct comment placement
				p.print(s.Value.Pos(), token.COMMA, blank)
				p.expr(s.Value)
			}
			p.print(blank, s.TokPos, s.Tok, blank)
		}
		p.print(token.RANGE, blank)
		p.expr(stripParens(s.X))
		p.print(blank)
		p.block(s.Body, 1)
	// ... (既存のコード) ...
}

コアとなるコードの解説

src/pkg/go/ast/ast.goの解説

ast.RangeStmt構造体の変更は、for range xという新しい構文をASTレベルで表現するための基盤を確立します。

  • Key, Value Expr // Key, Value may be nil: このコメントの変更は、Keyフィールドもnilになりうることを明示しています。これは、for range xのようにキー変数が存在しない場合に、RangeStmtKeyフィールドがnilとして表現されることを意味します。
  • TokPos token.Pos // position of Tok; invalid if Key == nil: TokPosは代入演算子(:=または=)の位置を示しますが、for range x構文ではこの演算子が存在しないため、Keynilの場合はこの位置情報が無効になることを示しています。
  • Tok token.Token // ILLEGAL if Key == nil, ASSIGN, DEFINE: Tokは代入演算子の種類を示しますが、Keynilの場合は代入演算子が存在しないため、token.ILLEGALという特殊なトークンが設定されることを示しています。これにより、ASTを処理する他のツールがこの特殊なケースを認識できるようになります。

これらの変更により、ASTはfor range xという構文を、キー変数が存在しないRangeStmtとして内部的に表現できるようになります。

src/pkg/go/parser/parser.goの解説

parseForStmt関数は、forループの構文解析を担当します。この変更の核心は、for range xという新しいパターンを認識し、それに対応するASTノードを生成する部分です。

  • if p.tok == token.RANGE: パーサーがforキーワードの直後にrangeキーワードを検出した場合、それはfor range x構文であると判断します。
  • y := []ast.Expr{&ast.UnaryExpr{OpPos: pos, Op: token.RANGE, X: p.parseRhs()}}: ここが重要な部分です。for range xは、内部的には左辺がない代入文として扱われます。ast.UnaryExprは単項演算子を表すASTノードですが、ここではOptoken.RANGEに設定することで、レンジ式自体を表現しています。p.parseRhs()はレンジ対象の式(この場合はx)を解析します。このUnaryExprast.AssignStmtRhs(右辺)に設定されます。これにより、パーサーはfor range xを、あたかもfor _ = range xのように、左辺が暗黙的に破棄される形式としてASTを構築します。
  • switch len(as.Lhs)case 0:: これは、for range xのように左辺の式が全くない場合を処理します。このケースでは、特別な処理は不要であり、keyvaluenilのままになります。これにより、ast.RangeStmtKeyフィールドがnilになることが保証されます。

src/pkg/go/printer/nodes.goの解説

printerパッケージはASTをGoソースコードに変換します。この変更は、ast.RangeStmtノードを整形する際に、Keyフィールドがnilの場合にキー変数と代入演算子を出力しないようにします。

  • if s.Key != nil: この条件文が追加されたことで、プリンターはRangeStmtKeyフィールドがnilでない場合(つまり、for key, value := range xfor key := range xのようにキー変数が明示的に存在する場合)にのみ、キー変数と代入演算子を出力します。
  • p.print(token.RANGE, blank): Keynilの場合でも、rangeキーワードとレンジ式は常に出力されます。

このロジックにより、パーサーが生成したKeynilRangeStmtが、正しくfor range xという簡潔な構文として出力されるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • go/ast, go/parser, go/printerパッケージのGoDoc
  • コミットの差分情報
  • Go言語のfor...rangeに関する一般的な解説記事 (Web検索結果)I have generated the detailed explanation based on the commit data and the specified chapter structure. I have also incorporated the necessary technical details and prerequisite knowledge. I will now output the explanation to standard output.