[インデックス 11651] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc)において、型スイッチ(type switch)の構文でブランク識別子 _ を使用することを禁止する変更を導入しています。具体的には、switch _ := v.(type) のような記述をコンパイルエラーとする修正です。
コミット
commit 74ee51ee92d35ccc6486b9126265bd2c62be2c3f
Author: Russ Cox <rsc@golang.org>
Date: Mon Feb 6 12:35:29 2012 -0500
cmd/gc: disallow switch _ := v.(type)
Fixes #2827.
R=ken2
CC=golang-dev
https://golang.org/cl/5638045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/74ee51ee92d35ccc6486b9126265bd2c62be2c3f
元コミット内容
このコミットの元々の内容は、Goコンパイラ(cmd/gc)が型スイッチの構文において、ブランク識別子 _ を変数名として使用することを許可していた問題を修正するものです。具体的には、switch _ := v.(type) という形式の型スイッチをコンパイル時にエラーとして扱うように変更されました。
変更の背景
この変更は、Go言語のIssue 2827「cmd/gc: disallow switch _ := v.(type)」を修正するために行われました。
Go言語の型スイッチは、インターフェース型の変数が実行時にどの具体的な型を持つかを判別し、それに応じた処理を行うための強力な構文です。通常、型スイッチでは、switch x := v.(type) のように、型アサーションの結果を新しい変数 x に代入し、その変数 x をケース節内で使用します。この x は、そのケース節内でのみ有効なスコープを持つ新しい変数であり、その型は対応するケースの型に推論されます。
しかし、Go言語には「ブランク識別子(blank identifier)」_ という特殊な識別子が存在します。これは、値を破棄したい場合や、変数を宣言したが使用しない場合にコンパイラのエラーを回避するために使用されます。例えば、複数の戻り値を持つ関数で一部の戻り値が不要な場合や、インポートしたパッケージの副作用だけを利用したい場合などに使われます。
Issue 2827では、switch _ := v.(type) という構文が許可されていることが問題視されました。この構文は、型スイッチの結果として得られる値をブランク識別子 _ に代入することを意味します。しかし、型スイッチの目的は、特定の型の場合にその型の値を変数にバインドし、その変数を使って処理を行うことです。ブランク識別子に代入するということは、その値を「破棄」することを意味し、型スイッチの本来の意図と矛盾します。
さらに重要な点として、型スイッチのケース節では、_ に代入された値が利用されることはありません。これは、_ が変数を宣言する場所ではなく、単に値を破棄する場所であるためです。したがって、switch _ := v.(type) は、実質的に型アサーションの結果を無視し、単に型の一致をチェックするだけの意味合いになります。これは、switch v.(type) と書くことで達成できることであり、_ := を使うことは冗長であり、誤解を招く可能性がありました。
このため、コンパイラがこのような構文を検出した場合に、開発者に対して「無効な変数名 _」であるというエラーを出すことで、より明確で意図に沿ったコード記述を促すことが決定されました。
前提知識の解説
Go言語の switch ステートメント
Go言語の switch ステートメントは、他の言語のそれと似ていますが、いくつかの特徴的な違いがあります。
breakの自動挿入: 各case節の最後に暗黙的にbreakが挿入されるため、次のcase節にフォールスルーすることはありません。明示的にフォールスルーさせたい場合はfallthroughキーワードを使用します。- 複数の式:
case節には複数の式をカンマ区切りで指定できます。 - 式なしの
switch:switchキーワードの後に式を省略すると、switch trueと同じ意味になり、各case節がブール式として評価されます。最初にtrueと評価されたcase節が実行されます。
Go言語の型スイッチ(Type Switch)
型スイッチは、インターフェース型の変数が実行時にどの具体的な型を持つかを判別するために使用される switch ステートメントの特殊な形式です。構文は以下のようになります。
switch v := i.(type) {
case int:
// i は int 型であり、v は int 型の値を持つ
fmt.Printf("i は int 型で、値は %d\n", v)
case string:
// i は string 型であり、v は string 型の値を持つ
fmt.Printf("i は string 型で、値は %s\n", v)
default:
// i は上記のどの型でもない
fmt.Printf("i は未知の型です (%T)\n", v)
}
ここで、i.(type) は型アサーションの一種で、switch ステートメントの制御式としてのみ使用できます。v := i.(type) のように新しい変数 v を宣言すると、各 case 節内でその v が対応する型に推論され、その型の値として利用できます。
Go言語のブランク識別子 _ (Blank Identifier)
ブランク識別子 _ は、Go言語における特殊な識別子で、値を破棄するために使用されます。
- 戻り値の破棄: 複数の戻り値を持つ関数で、一部の戻り値が不要な場合に
_を使用して破棄できます。_, err := strconv.Atoi("123") // 変換された値は不要で、エラーだけをチェックしたい - 未使用変数の回避: 変数を宣言したが使用しない場合に、コンパイラが「未使用変数」のエラーを出すのを防ぐために
_に代入します。var x int _ = x // x は使用されないが、エラーにならない - インポートの副作用: パッケージをインポートする際に、そのパッケージの初期化関数(
init)だけを実行したいが、パッケージ内の識別子を直接使用しない場合に_を使用します。import _ "net/http/pprof" // pprof のHTTPハンドラを登録するだけ
ブランク識別子は、変数を宣言する場所ではなく、値を「捨てる」場所として機能します。そのため、_ に代入された値は、その後のコードで参照することはできません。
技術的詳細
このコミットの技術的詳細は、Goコンパイラの字句解析器と構文解析器の挙動に深く関連しています。Goコンパイラのフロントエンドは、Yacc(Yet Another Compiler Compiler)によって生成されたパーサーを使用しています。
src/cmd/gc/go.y: これはGo言語の文法規則を定義するYaccの入力ファイルです。Goコンパイラは、このファイルに記述された文法規則に基づいてソースコードを解析し、抽象構文木(AST)を構築します。src/cmd/gc/y.tab.c: これはgo.yからYaccによって自動生成されるC言語のソースファイルで、実際の構文解析ロジックを含んでいます。
変更の核心は、型スイッチの制御式 v := i.(type) の部分で、左辺の変数名がブランク識別子 _ であるかどうかをチェックするロジックを追加することです。
既存のコードでは、型スイッチの左辺の変数名が ONAME(通常の変数名)、OTYPE(型名)、または ONONAME(匿名変数)であるかどうかをチェックしていました。これらのいずれでもない場合に「無効な変数名」としてエラーを出力していました。
このコミットでは、このチェックに加えて、isblank($1->n) という条件が追加されています。
$1->nは、構文解析中のノード(ここでは型スイッチの左辺の変数名を表すノード)を指します。isblank()関数は、与えられたノードがブランク識別子_を表すかどうかを判定するヘルパー関数です。
つまり、変更後のロジックは以下のようになります。
「型スイッチの左辺の変数名が ONAME、OTYPE、ONONAME のいずれでもなく、かつ、それがブランク識別子 _ である場合」にエラーを発生させる。
これは、_ が ONAME などとは異なる特殊な識別子として扱われるため、既存のチェックでは捕捉できなかった _ の使用を明示的に禁止するためのものです。_ は通常の変数名とは異なるセマンティクスを持つため、型スイッチの文脈では「無効な変数名」として扱われるべきであるという設計判断が反映されています。
この変更により、コンパイラは switch _ := v.(type) のようなコードを検出した際に、invalid variable name _ というエラーメッセージを出力するようになります。これにより、開発者は型スイッチの正しい使用方法を促され、意図しないコードの記述を防ぐことができます。
コアとなるコードの変更箇所
src/cmd/gc/go.y の変更
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -423,7 +423,7 @@ simple_stmt:
yyerror("expr.(type) must be alone in list");
if($1->next != nil)
yyerror("argument count mismatch: %d = %d", count($1), 1);
- else if($1->n->op != ONAME && $1->n->op != OTYPE && $1->n->op != ONONAME)
+ else if(($1->n->op != ONAME && $1->n->op != OTYPE && $1->n->op != ONONAME) || isblank($1->n))
yyerror("invalid variable name %N in type switch", $1->n);
else
$$->left = dclname($1->n->sym); // it's a colas, so must not re-use an oldname.
この変更は、simple_stmt ルール内の型スイッチに関する部分にあります。else if 節の条件に || isblank($1->n) が追加されています。これは、型スイッチの左辺のノード $1->n が通常の変数名 (ONAME)、型名 (OTYPE)、匿名変数 (ONONAME) のいずれでもない場合に加えて、それがブランク識別子である場合もエラーとする条件です。
src/cmd/gc/y.tab.c の変更
--- a/src/cmd/gc/y.tab.c
+++ b/src/cmd/gc/y.tab.c
@@ -2714,7 +2714,7 @@ yyreduce:
yyerror("expr.(type) must be alone in list");
if((yyvsp[(1) - (3)].list)->next != nil)
yyerror("argument count mismatch: %d = %d", count((yyvsp[(1) - (3)].list)), 1);
- else if((yyvsp[(1) - (3)].list)->n->op != ONAME && (yyvsp[(1) - (3)].list)->n->op != OTYPE && (yyvsp[(1) - (3)].list)->n->op != ONONAME)
+ else if(((yyvsp[(1) - (3)].list)->n->op != ONAME && (yyvsp[(1) - (3)].list)->n->op != OTYPE && (yyvsp[(1) - (3)].list)->n->op != ONONAME) || isblank((yyvsp[(1) - (3)].list)->n))
yyerror("invalid variable name %N in type switch", (yyvsp[(1) - (3)].list)->n);
else
(yyval.node)->left = dclname((yyvsp[(1) - (3)].list)->n->sym); // it's a colas, so must not re-use an oldname.
y.tab.c は go.y から自動生成されるファイルであるため、go.y の変更が反映されています。同様に、else if 節の条件に || isblank((yyvsp[(1) - (3)].list)->n) が追加されています。
test/typeswitch3.go の変更
--- a/test/typeswitch3.go
+++ b/test/typeswitch3.go
@@ -30,6 +30,10 @@ func main(){
switch r.(type) {
case io.Writer:
}
+
+ // Issue 2827.
+ switch _ := r.(type) { // ERROR "invalid variable name _"
+ }
}
このテストファイルには、// Issue 2827. というコメントとともに、問題となっていた switch _ := r.(type) の行が追加されています。その行の末尾には // ERROR "invalid variable name _" というコメントがあり、これはGoのテストフレームワークがこの行で指定されたエラーメッセージが出力されることを期待していることを示しています。これにより、コンパイラが正しくエラーを出すことを検証します。
コアとなるコードの解説
このコミットのコアとなる変更は、Goコンパイラの構文解析ロジックに isblank() 関数によるチェックを追加した点です。
Go言語のコンパイラは、ソースコードを解析する際に、各要素をノードとして表現します。型スイッチの左辺に現れる変数名もまた、シンボルテーブル内のエントリと関連付けられたノードとして扱われます。
変更前のコンパイラは、型スイッチの左辺の識別子が ONAME(通常の変数)、OTYPE(型)、ONONAME(匿名変数)のいずれかであることを期待していました。これらのいずれでもない場合、コンパイラは「無効な変数名」としてエラーを報告していました。
しかし、ブランク識別子 _ は、通常の変数名とは異なる特殊なセマンティクスを持つため、既存の ONAME などのチェックでは適切に扱われませんでした。_ は変数を宣言するものではなく、値を破棄するための構文要素です。型スイッチの目的は、型アサーションの結果を新しい変数にバインドし、その変数をケース節内で利用することにあるため、値を破棄する _ を使用することは、型スイッチの意図に反します。
そこで、isblank($1->n) という条件が追加されました。
$1->nは、構文解析中の型スイッチの左辺のノードです。isblank()関数は、このノードがブランク識別子_を表すかどうかを判定します。
この追加された条件により、もし型スイッチの左辺がブランク識別子 _ であった場合、既存のチェック(ONAME などではない)と組み合わさって、コンパイラは明示的に「invalid variable name _」というエラーを発生させるようになります。
この修正は、Go言語の設計思想である「明確さ」と「意図の明確化」を反映しています。switch _ := v.(type) は、_ が変数を宣言する場所ではないというGoの基本的なルールと矛盾し、また、型スイッチの本来の目的(型に応じた値のバインドと利用)とも合致しません。この変更により、コンパイラが開発者に対してより厳密なフィードバックを提供し、Go言語の慣用的な記述方法を促すことになります。
関連リンク
- Go言語 Issue 2827: https://github.com/golang/go/issues/2827
- Go言語の型スイッチに関する公式ドキュメント(Go言語仕様): https://go.dev/ref/spec#Type_switches
- Go言語のブランク識別子に関する公式ドキュメント(Go言語仕様): https://go.dev/ref/spec#Blank_identifier
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gc/ディレクトリ) - Go言語のIssueトラッカー
- Yacc (Yet Another Compiler Compiler) の一般的な知識
- Go言語仕様 (The Go Programming Language Specification)