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

[インデックス 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 ステートメントは、他の言語のそれと似ていますが、いくつかの特徴的な違いがあります。

  1. break の自動挿入: 各 case 節の最後に暗黙的に break が挿入されるため、次の case 節にフォールスルーすることはありません。明示的にフォールスルーさせたい場合は fallthrough キーワードを使用します。
  2. 複数の式: case 節には複数の式をカンマ区切りで指定できます。
  3. 式なしの 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() 関数は、与えられたノードがブランク識別子 _ を表すかどうかを判定するヘルパー関数です。

つまり、変更後のロジックは以下のようになります。

「型スイッチの左辺の変数名が ONAMEOTYPEONONAME のいずれでもなく、かつ、それがブランク識別子 _ である場合」にエラーを発生させる。

これは、_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.cgo.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言語のソースコード(特に src/cmd/gc/ ディレクトリ)
  • Go言語のIssueトラッカー
  • Yacc (Yet Another Compiler Compiler) の一般的な知識
  • Go言語仕様 (The Go Programming Language Specification)