[インデックス 10305] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)におけるswitch
文の型チェックメカニズムを改善し、より堅牢なエラー検出と柔軟な型アサーションを可能にするものです。特に、インターフェース値に対するswitch
文での型チェックの厳密化と、型スイッチにおける早期の静的チェックに焦点を当てています。
コミット
commit 13e92e4d7542ac65a7efb33778f752403c5ac014
Author: Luuk van Dijk <lvd@golang.org>
Date: Wed Nov 9 10:58:53 2011 +0100
gc: Better typechecks and errors in switches.
Allow any type in switch on interface value.
Statically check typeswitch early.
Fixes #2423.
Fixes #2424.
R=rsc, dsymonds
CC=golang-dev
https://golang.org/cl/5339045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13e92e4d7542ac65a7efb33778f752403c5ac014
元コミット内容
このコミットの元のメッセージは以下の通りです。
gc: Better typechecks and errors in switches.
Allow any type in switch on interface value. Statically check typeswitch early.
Fixes #2423. Fixes #2424.
これは、Goコンパイラ(gc
)におけるswitch
文の型チェックとエラー報告を改善することを目的としています。具体的には、インターフェース値に対するswitch
文で任意の型を許可すること、そして型スイッチ(type switch)において早期に静的チェックを行うことを挙げています。これにより、Go言語の型システムがより堅牢になり、開発者がコンパイル時に潜在的な型不一致エラーを早期に発見できるようになります。
変更の背景
このコミットは、Go言語のswitch
文、特に型スイッチ(type switch)における既存のバグと制限に対処するために導入されました。コミットメッセージに記載されているFixes #2423
とFixes #2424
は、それぞれ以下の問題に対応しています。
-
Issue 2423:
switch
on interface value allows non-assignable types incase
clauses この問題は、インターフェース値に対する通常のswitch
文(型スイッチではない)において、case
句でテストされる値の型が、switch
式の型に割り当て可能であるかどうかのチェックが不十分であったことを示しています。これにより、実行時にパニックを引き起こす可能性のあるコードがコンパイルされてしまうという問題がありました。例えば、interface{}
型の変数に対してswitch
を行い、case
句でstring
型のリテラルを指定した場合、コンパイラはエラーを報告せず、実行時に型不一致が発生する可能性がありました。 -
Issue 2424:
type switch
allows impossible cases この問題は、型スイッチにおいて、決して到達しない(不可能な)case
句がコンパイル時に検出されなかったことを示しています。例えば、あるインターフェース型I
に対して型スイッチを行い、case
句でI
が実装していないメソッドを持つ型T
を指定した場合、コンパイラはエラーを報告せず、開発者が論理的な誤りに気づきにくい状況でした。これは、インターフェースのメソッドセットとcase
句で指定された型のメソッドセットとの互換性チェックが不十分であったことに起因します。
これらのバグは、Go言語の型安全性を損ない、開発者が予期せぬ実行時エラーに遭遇する原因となっていました。このコミットは、これらの問題を解決し、コンパイル時にこれらの型関連のエラーをより厳密にチェックすることで、Goプログラムの信頼性と堅牢性を向上させることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語およびコンパイラに関する前提知識が必要です。
-
Go言語の
switch
文: Go言語のswitch
文には主に2つの形式があります。- 式スイッチ (Expression Switch):
switch
キーワードの後に式が続き、その式の値とcase
句の値が比較されます。switch x { case 1: // x == 1 case 2: // x == 2 default: }
- 型スイッチ (Type Switch):
switch
キーワードの後に型アサーション.(type)
が続き、変数の動的な型に基づいて処理を分岐させます。これは主にインターフェース型に対して使用されます。switch v := i.(type) { case int: // v は int 型 case string: // v は string 型 default: // v は i の静的な型 }
- 式スイッチ (Expression Switch):
-
Go言語のインターフェース: Goのインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを実装していると見なされます(暗黙的な実装)。インターフェース型の変数は、任意の基になる具体的な型の値を保持できます。
-
Goコンパイラ (
gc
):gc
はGo言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などが含まれます。このコミットで変更されているsrc/cmd/gc/swt.c
は、gc
コンパイラのswitch
文の処理と型チェックを担当する部分です。 -
型チェック (Type Checking): 型チェックは、プログラムが型規則に従っていることを検証するコンパイルフェーズです。これにより、型不一致などのエラーをコンパイル時に検出し、実行時エラーを防ぎます。Goは静的型付け言語であり、厳密な型チェックが行われます。
-
assignop
関数: Goコンパイラ内部の関数で、ある型が別の型に割り当て可能(assignable)であるかをチェックします。例えば、int
型の値をfloat64
型の変数に割り当てることは可能ですが、その逆は直接はできません。この関数は、代入、関数呼び出しの引数渡し、case
句での値の比較など、様々な文脈で型の互換性を検証するために使用されます。 -
implements
関数: Goコンパイラ内部の関数で、ある型が特定のインターフェースを実装しているかどうかをチェックします。これは、インターフェースのメソッドセットと、チェック対象の型のメソッドセットを比較することで行われます。この関数は、型スイッチのcase
句で指定された型が、switch
式のインターフェース型によって保持されうる型であるかを検証する際に特に重要です。
これらの知識を前提として、このコミットがswitch
文の型チェックロジックをどのように修正し、Goプログラムの堅牢性を高めているかを詳細に見ていきます。
技術的詳細
このコミットの技術的詳細は、主にsrc/cmd/gc/swt.c
ファイルのtypecheckswitch
関数における変更に集約されます。この関数は、switch
文の型チェックロジックを実装しており、式スイッチと型スイッチの両方に対応しています。
変更の核心は、case
句の型とswitch
式の型の間の互換性チェックを強化することにあります。
1. 式スイッチ (case Erv
) における型チェックの改善
変更前は、式スイッチのcase
句において、ll->n->type != T && !eqtype(ll->n->type, t)
という条件で型が等しいかどうかのみをチェックしていました。これは、厳密な型の一致を要求するため、Goの柔軟な型変換ルール(例えば、異なる整数型間の比較や、数値型とインターフェース型間の比較など)を適切に扱えない場合がありました。
変更後、この行は以下のように修正されました。
- else if(ll->n->type != T && !eqtype(ll->n->type, t))
- yyerror("case %lN in %T switch", ll->n, t);
+ else if(ll->n->type != T && !assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)) {
+ if(n->ntest)
+ yyerror("invalid case %N in switch on %N (mismatched types %T and %T)", ll->n, n->ntest, ll->n->type, t);
+ else
+ yyerror("invalid case %N in switch (mismatched types %T and bool)", ll->n, n->ntest, ll->n->type, t);
+ }
この変更のポイントは以下の通りです。
assignop
関数の導入:!eqtype(ll->n->type, t)
の代わりに、!assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)
が使用されています。これは、case
式の型(ll->n->type
)がswitch
式の型(t
)に割り当て可能であるか、またはその逆であるかをチェックします。これにより、厳密な型の一致だけでなく、Goの型変換ルールに基づいた互換性も考慮されるようになります。例えば、int
型のswitch
式に対してint32
型のcase
値が指定された場合でも、assignop
がtrue
を返すため、エラーにならなくなります。これは、Issue 2423で指摘されたような、インターフェース値に対するswitch
で不適切な型が許可される問題を解決する一助となります。- より詳細なエラーメッセージ:
yyerror
のメッセージが、switch
式が存在するかどうかに応じて、より具体的で分かりやすいものに変更されました。これにより、開発者は型不一致の原因を特定しやすくなります。
2. 型スイッチ (case Etype
) における早期静的チェックの導入
型スイッチのcase
句では、nil
リテラルが特別扱いされます。それ以外のcase
句については、変更前はll->n->op != OTYPE && ll->n->type != T
という条件で、case
句が型ではない場合にエラーを報告していました。しかし、これはcase
句で指定された型が、switch
式のインターフェース型によって実際に保持されうる型であるかどうかのチェックが不十分でした。
変更後、以下のelse if
ブロックが追加されました。
+ } else if(!implements(ll->n->type, t, &missing, &have, &ptr)) {
+ if(have && !missing->broke && !have->broke)
+ yyerror("impossible type switch case: %lN cannot have dynamic type %T"\
+ " (wrong type for %S method)\\n\\thave %S%hT\\n\\twant %S%hT",\
+ n->ntest->right, ll->n->type, missing->sym, have->sym, have->type,\
+ missing->sym, missing->type);
+ else if(!missing->broke)
+ yyerror("impossible type switch case: %lN cannot have dynamic type %T"\
+ " (missing %S method)", n->ntest->right, ll->n->type, missing->sym);
+ }
この変更のポイントは以下の通りです。
implements
関数の利用:!implements(ll->n->type, t, ...)
という条件が追加されました。ここで、ll->n->type
はcase
句で指定された型、t
はswitch
式のインターフェース型です。implements
関数は、ll->n->type
がt
インターフェースを実装しているかどうかをチェックします。もし実装していない場合、つまりcase
句で指定された型がswitch
式のインターフェース型によって保持され得ない「不可能なケース」である場合、エラーが報告されます。これはIssue 2424で指摘された問題を直接解決します。- 詳細なエラー報告:
implements
関数が返す情報(missing
、have
)を利用して、エラーメッセージが非常に詳細になっています。wrong type for %S method
:case
句の型がインターフェースの特定のメソッドを実装しているが、そのシグネチャがインターフェースの定義と一致しない場合に報告されます。missing %S method
:case
句の型がインターフェースの特定のメソッドを完全に実装していない場合に報告されます。 これにより、開発者はなぜそのcase
が不可能であるのかを正確に理解できます。
3. テストファイルの変更
このコミットでは、既存のバグを修正し、新しいチェックが正しく機能することを確認するために、複数のテストファイルが変更されています。
test/fixedbugs/bug270.go
が削除されました。これは、型スイッチにおけるインターフェースのcase
句に関するテストでしたが、このコミットの変更によって不要になったか、あるいは新しいテストでより適切にカバーされるようになったためと考えられます。test/fixedbugs/bug340.go
が修正されました。このファイルは、型スイッチにおける誤ったcase
句の型に関するテストで、エラーメッセージが新しいコンパイラの出力に合わせて更新されています。test/fixedbugs/bug375.go
が新規追加されました。これはIssue 2423に関連するテストで、インターフェース値に対する式スイッチで文字列リテラルが正しく扱われることを確認します。test/switch3.go
が新規追加されました。これは、式スイッチにおける型不一致のケースをテストし、assignop
による新しいチェックが期待通りに機能することを確認します。test/typeswitch3.go
が新規追加されました。これは、型スイッチにおける「不可能なケース」をテストし、implements
による新しい静的チェックが正しくエラーを報告することを確認します。
これらの変更により、Goコンパイラはswitch
文、特に型スイッチにおいて、より厳密で情報量の多い型チェックを行うようになり、開発者がコンパイル時に潜在的な問題を早期に発見できるようになりました。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更箇所は、src/cmd/gc/swt.c
ファイルのtypecheckswitch
関数内です。
具体的には、以下の2つの主要な変更点があります。
-
式スイッチ (
case Erv
) の型チェックロジックの変更:swt.c
の859行目から868行目にかけての変更です。--- a/src/cmd/gc/swt.c +++ b/src/cmd/gc/swt.c @@ -854,21 +854,35 @@ typecheckswitch(Node *n) t = typecheck(&ll->n, Erv | Etype); if(ll->n->type == T || t == T) continue; + setlineno(ncase); switch(top) { case Erv: // expression switch defaultlit(&ll->n, t); if(ll->n->op == OTYPE) yyerror("type %T is not an expression", ll->n->type); - else if(ll->n->type != T && !eqtype(ll->n->type, t)) - yyerror("case %lN in %T switch", ll->n, t); + else if(ll->n->type != T && !assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)) { + if(n->ntest) + yyerror("invalid case %N in switch on %N (mismatched types %T and %T)", ll->n, n->ntest, ll->n->type, t); + else + yyerror("invalid case %N in switch (mismatched types %T and bool)", ll->n, n->ntest, ll->n->type, t); + } break;
ここで、
!eqtype(ll->n->type, t)
が!assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)
に置き換えられ、より柔軟な型互換性チェックが導入されました。 -
型スイッチ (
case Etype
) の早期静的チェックの追加:swt.c
の871行目から889行目にかけての追加です。--- a/src/cmd/gc/swt.c +++ b/src/cmd/gc/swt.c @@ -854,21 +854,35 @@ typecheckswitch(Node *n) t = typecheck(&ll->n, Erv | Etype); if(ll->n->type == T || t == T) continue; + setlineno(ncase); switch(top) { case Erv: // expression switch defaultlit(&ll->n, t); if(ll->n->op == OTYPE) yyerror("type %T is not an expression", ll->n->type); - else if(ll->n->type != T && !eqtype(ll->n->type, t)) - yyerror("case %lN in %T switch", ll->n, t); + else if(ll->n->type != T && !assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)) { + if(n->ntest) + yyerror("invalid case %N in switch on %N (mismatched types %T and %T)", ll->n, n->ntest, ll->n->type, t); + else + yyerror("invalid case %N in switch (mismatched types %T and bool)", ll->n, n->ntest, ll->n->type, t); + } break; case Etype: // type switch if(ll->n->op == OLITERAL && istype(ll->n->type, TNIL)) { ; - } else if(ll->n->op != OTYPE && ll->n->type != T) {\n+\t\t\t\t\t} else if(ll->n->op != OTYPE && ll->n->type != T) { // should this be ||?\n \t\t\t\t\t\tyyerror("%lN is not a type", ll->n);\n \t\t\t\t\t\t// reset to original type\n \t\t\t\t\t\tll->n = n->ntest->right;\n+\t\t\t\t\t} else if(!implements(ll->n->type, t, &missing, &have, &ptr)) {\n+\t\t\t\t\t\tif(have && !missing->broke && !have->broke)\n+\t\t\t\t\t\t\tyyerror("impossible type switch case: %lN cannot have dynamic type %T"\n+\t\t\t\t\t\t\t\t" (wrong type for %S method)\\n\\thave %S%hT\\n\\twant %S%hT",\n+\t\t\t\t\t\t\t\tn->ntest->right, ll->n->type, missing->sym, have->sym, have->type,\n+\t\t\t\t\t\t\t\tmissing->sym, missing->type);\n+\t\t\t\t\t\telse if(!missing->broke)\n+\t\t\t\t\t\t\tyyerror("impossible type switch case: %lN cannot have dynamic type %T"\n+\t\t\t\t\t\t\t\t" (missing %S method)", n->ntest->right, ll->n->type, missing->sym);\n }\n break; }
この追加された
else if
ブロック内で、implements
関数が呼び出され、case
句の型がswitch
式のインターフェース型を実装しているかどうかがチェックされます。これにより、「不可能なケース」がコンパイル時に検出されるようになります。
これらの変更は、Goコンパイラの型チェックの厳密性を高め、より多くの型関連のエラーをコンパイル時に捕捉することを可能にしています。
コアとなるコードの解説
src/cmd/gc/swt.c
は、Goコンパイラ(gc
)のバックエンドの一部であり、switch
文のセマンティック分析とコード生成に関連する処理を扱います。このファイル内のtypecheckswitch
関数は、switch
文の各case
句の型が、switch
式の型と互換性があるかを検証する役割を担っています。
typecheckswitch
関数の役割
typecheckswitch
関数は、抽象構文木(AST)上のswitch
ノードを受け取り、その子ノード(case
句など)を走査しながら型チェックを行います。この関数は、式スイッチと型スイッチの両方を処理します。
top
変数:switch
文の種類(Erv
は式スイッチ、Etype
は型スイッチ)を識別します。ncase
: 現在処理しているcase
句のノード。ll->n
:case
句で指定された値または型。t
:switch
式の型。
変更点の詳細な解説
-
式スイッチにおける
assignop
の利用: 変更前は、式スイッチのcase
句の型チェックにeqtype
(型が完全に等しいか)を使用していました。しかし、Go言語では、異なるが互換性のある型(例:int
とint32
、または基になる型が同じカスタム型)間での比較や代入が可能です。eqtype
ではこのような柔軟な互換性を捉えきれませんでした。導入された
!assignop(ll->n->type, t, nil) && !assignop(t, ll->n->type, nil)
は、以下のロジックを意味します。assignop(ll->n->type, t, nil)
:case
句の型(ll->n->type
)がswitch
式の型(t
)に割り当て可能か?assignop(t, ll->n->type, nil)
:switch
式の型(t
)がcase
句の型(ll->n->type
)に割り当て可能か?
この2つの条件を
&&
で結合し、全体を!
で否定することで、「どちらの方向にも割り当て可能でない場合」にエラーを報告します。これにより、Goの型変換ルールに則ったより適切な型互換性チェックが実現され、Issue 2423のような問題(インターフェース値に対するswitch
で不適切な型が許可される)が解決されます。例えば、interface{}
型の変数に対するswitch
でstring
リテラルをcase
に指定した場合、string
はinterface{}
に割り当て可能であるため、このチェックは通過します。しかし、int
型のswitch
に対してstring
リテラルをcase
に指定した場合は、どちらの方向にも割り当て不可能であるため、エラーが報告されます。 -
型スイッチにおける
implements
の利用: 型スイッチでは、switch
式のインターフェース型が、case
句で指定された動的な型を実際に保持できるかどうかが重要です。変更前は、case
句が型ではない場合にエラーを出すだけでした。しかし、case
句が型であったとしても、その型がswitch
式のインターフェースを実装していなければ、そのcase
は決して到達しません(「不可能なケース」)。追加された
else if(!implements(ll->n->type, t, &missing, &have, &ptr))
ブロックは、この「不可能なケース」を検出します。implements(ll->n->type, t, ...)
:case
句の型(ll->n->type
)がswitch
式のインターフェース型(t
)を実装しているかをチェックします。missing
:ll->n->type
がt
を実装するために不足しているメソッドの情報。have
:ll->n->type
がt
のメソッドに対応して持っているメソッドの情報。
implements
関数がfalse
を返した場合(つまり、ll->n->type
がt
インターフェースを実装していない場合)、コンパイラは「不可能な型スイッチケース」としてエラーを報告します。エラーメッセージは、missing
やhave
の情報に基づいて、どのメソッドが不足しているのか、あるいはどのメソッドの型が間違っているのかを具体的に示します。これにより、Issue 2424で指摘されたような、決して到達しないcase
句がコンパイルされてしまう問題が解決され、開発者は論理的な誤りを早期に発見できるようになります。
これらの変更は、Go言語の型システムが提供する安全性をコンパイル時に最大限に活用し、実行時エラーのリスクを低減するための重要な改善です。
関連リンク
- Go Issue 2423:
switch
on interface value allows non-assignable types incase
clauses- https://code.google.com/p/go/issues/detail?id=2423 (現在はGitHubに移行)
- Go Issue 2424:
type switch
allows impossible cases- https://code.google.com/p/go/issues/detail?id=2424 (現在はGitHubに移行)
- Go CL 5339045: gc: Better typechecks and errors in switches.
- https://golang.org/cl/5339045 (Gerrit Code Reviewへのリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント:
switch
文 - Go言語の公式ドキュメント: インターフェース型
- Goコンパイラのソースコード(
src/cmd/compile/internal/gc/
ディレクトリ以下)- 特に
src/cmd/compile/internal/gc/swt.go
(現在のGoコンパイラではswt.c
はswt.go
に移行しています) - https://github.com/golang/go/tree/master/src/cmd/compile/internal/gc
- 特に
- Goコンパイラの型チェックに関する一般的な情報
- Goのコンパイラがどのように型チェックを行うかについてのブログ記事や解説記事(一般的な情報源)
- Go言語の
assignable
ルールに関する情報 - Go言語の
implements
ルールに関する情報- https://go.dev/ref/spec#Interface_types (インターフェースの実装に関するセクション)
- Go言語のバグトラッカー(GitHub Issues)
- Go言語のコードレビューシステム(Gerrit)