[インデックス 14576] ファイルの概要
このコミットは、Go言語の仕様書(doc/go_spec.html
)を更新し、型アサーションと型スイッチに関する記述を明確化するものです。具体的には、コンクリート型がインターフェース型を実装していない無効な型アサーションや型スイッチが、仕様上は明示的に禁止されていなかった点を修正し、コンパイラ(gcおよびgccgo)の既存の振る舞い(これらの無効なケースを除外する)を仕様に反映させています。これにより、仕様と実装の整合性が向上し、開発者がより正確にGo言語の型システムを理解できるようになります。また、関連する例の軽微なクリーンアップと、select
ステートメントにおけるdefault
ケースに関する記述の追加も行われています。
コミット
commit 485673188dc6c3ee3113990ed1e96ca8f8f0df51
Author: Robert Griesemer <gri@golang.org>
Date: Thu Dec 6 09:17:20 2012 -0800
spec: type assertions and type switches must be valid
The spec didn't preclude invalid type assertions and
type switches, i.e., cases where a concrete type doesn't
implement the interface type in the assertion in the first
place. Both, the gc and gccgo compiler exclude these cases.
This is documenting the status quo.
Also:
- minor clean up of respective examples
- added sentence about default case in select statements
Fixes #4472.
R=rsc, iant, r, ken
CC=golang-dev
https://golang.org/cl/6869050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/485673188dc6c3ee3113990ed1e96ca8f8f0df51
元コミット内容
このコミットの元々の意図は、Go言語の仕様において、無効な型アサーションと型スイッチが明示的に禁止されていないという曖昧さを解消することでした。具体的には、型アサーションや型スイッチの対象となるコンクリート型が、アサーションやスイッチで指定されたインターフェース型をそもそも実装していない場合、Goのコンパイラ(gcおよびgccgo)はこれらのケースをエラーとして扱っていました。しかし、当時の仕様書にはこの振る舞いが明記されていなかったため、仕様と実装の間に乖離がありました。このコミットは、この「現状」(status quo)を仕様書に文書化し、整合性を取ることを目的としています。
さらに、型アサーションと型スイッチに関する既存の例をより明確にするための軽微な修正と、select
ステートメントにおけるdefault
ケースの記述の追加も含まれています。
変更の背景
Go言語の設計思想の一つに「仕様と実装の一致」があります。しかし、このコミットがなされる以前は、型アサーションと型スイッチに関して、仕様とコンパイラの振る舞いの間に不一致が存在していました。
具体的には、Goの型アサーション x.(T)
は、x
がインターフェース型であり、その動的な型がT
であるか、またはx
の動的な型がインターフェースT
を実装しているかをチェックします。同様に、型スイッチ switch x.(type)
は、x
の動的な型に基づいて異なるケースに分岐します。
問題は、もしT
がインターフェース型ではないコンクリート型であり、かつT
がx
のインターフェース型を実装していない場合、あるいは型スイッチのcase
で指定されたコンクリート型が、スイッチ対象のインターフェース型を実装していない場合、コンパイラはこれを無効な操作としてエラーを報告していました。これは論理的に考えても当然の振る舞いですが、当時のGo言語の仕様書には、このような「不可能な」型アサーションや型スイッチが明示的に禁止されていませんでした。
この仕様の曖昧さは、Go言語の学習者や開発者にとって混乱の原因となる可能性がありました。コンパイラがエラーを出すにもかかわらず、仕様書にはその根拠が明確に書かれていないためです。このコミットは、このギャップを埋め、コンパイラの既存の振る舞いを仕様書に明記することで、Go言語の定義をより堅牢で明確なものにすることを目的としています。
また、コミットメッセージにある Fixes #4472
は、GoのIssueトラッカーにおける特定の課題を指していますが、現在のGoのIssueトラッカーではこの番号のIssueは見つかりません。これは、Issueトラッカーの移行や番号の再割り当てなど、時間の経過による変更が原因である可能性があります。しかし、コミットメッセージの内容から、この変更が特定の報告された問題に対応するものであることは明らかです。
前提知識の解説
このコミットを理解するためには、Go言語における以下の概念を理解しておく必要があります。
1. インターフェース (Interfaces)
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goでは、型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを「実装している」とみなされます(暗黙的な実装)。これにより、異なる具体的な型が同じインターフェースを満たすことで、ポリモーフィックな振る舞いを実現できます。
例:
type Reader interface {
Read(p []byte) (n int, err error)
}
type FileWriter struct { /* ... */ }
func (fw *FileWriter) Read(p []byte) (n int, err error) { /* ... */ } // FileWriterはReaderインターフェースを実装
2. 型アサーション (Type Assertions)
型アサーションは、インターフェース型の変数が保持している動的な値の基底となる具体的な型を調べたり、その値を別のインターフェース型に変換したりするために使用されます。構文は x.(T)
です。
-
T
が非インターフェース型の場合:x.(T)
は、x
が保持する動的な値の型がT
と同一であることをアサートします。成功した場合、T
型の値が返されます。失敗した場合、ランタイムパニックが発生します。- 例:
var i interface{} = 10; j := i.(int)
(成功) - 例:
var i interface{} = "hello"; j := i.(int)
(パニック)
- 例:
-
T
がインターフェース型の場合:x.(T)
は、x
が保持する動的な値の型がインターフェースT
を実装していることをアサートします。成功した場合、T
型の値が返されます。失敗した場合、ランタイムパニックが発生します。- 例:
var r io.Reader = &bytes.Buffer{}; reader := r.(io.ByteReader)
(成功)
- 例:
型アサーションには、成功/失敗をチェックするための2値の結果を返す形式もあります: value, ok := x.(T)
。ok
が true
の場合、アサーションは成功し、value
には変換された値が格納されます。ok
が false
の場合、アサーションは失敗し、value
には T
のゼロ値が格納されます。この形式ではパニックは発生しません。
3. 型スイッチ (Type Switches)
型スイッチは、インターフェース型の変数が保持する動的な値の型に基づいて、異なるコードブロックを実行するための制御構造です。構文は switch x.(type)
です。
例:
func doSomething(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}
型スイッチの各case
では、特定の型を指定できます。case
で指定された型がインターフェース型でない場合、その型はスイッチ対象のインターフェース型を実装している必要があります。
4. select
ステートメント
select
ステートメントは、複数の通信操作(チャネルの送受信)を待機し、そのうちのいずれかが準備できたときに実行されるGoの制御構造です。select
は、switch
ステートメントに似ていますが、case
がチャネル操作である点が異なります。
default
ケース:select
ステートメントにはオプションでdefault
ケースを含めることができます。default
ケースが存在する場合、どのチャネル操作もすぐに実行できないときに、default
ケースが実行されます。default
ケースがない場合、select
ステートメントはチャネル操作のいずれかが準備できるまでブロックします。
技術的詳細
このコミットの技術的詳細の核心は、Go言語の仕様書 doc/go_spec.html
における型アサーションと型スイッチのセクションに、以下の制約を明示的に追加した点にあります。
型アサーションの変更点
型アサーション x.(T)
のセクションにおいて、T
が非インターフェース型の場合の記述に以下の文が追加されました。
In this case, <code>T</code> must <a href="#Method_sets">implement</a> the (interface) type of <code>x</code>;
otherwise the type assertion is invalid since it is not possible for <code>x</code>
to store a value of type <code>T</code>.
これは、「この場合(T
が非インターフェース型の場合)、T
はx
の(インターフェース)型を実装していなければならない。そうでなければ、x
がT
型の値を格納することは不可能であるため、型アサーションは無効である」という意味です。
この追加により、例えば var y I; s := y.(string)
のようなコードがなぜ無効であるかが明確になります。ここで I
は m()
メソッドを持つインターフェースであり、string
型は m()
メソッドを実装していないため、string
はインターフェース I
を実装していません。したがって、y
が string
型の値を保持することは論理的に不可能です。この仕様の追加は、コンパイラが既にこのようなケースをエラーとして扱っていた振る舞いを仕様に反映させたものです。
また、型アサーションの例もクリーンアップされ、より明確な例が追加されました。
var x interface{} = 7 // x has dynamic type int and value 7
i := x.(int) // i has type int and value 7
type I interface { m() }
var y I
s := y.(string) // illegal: string does not implement I (missing method m)
r := y.(io.Reader) // r has type io.Reader and y must implement both I and io.Reader
特に s := y.(string)
の行に illegal: string does not implement I (missing method m)
というコメントが追加され、無効なケースが具体的に示されています。
型スイッチの変更点
型スイッチ switch x.(type)
のセクションにおいても、同様の制約が追加されました。
Cases then match actual types <code>T</code> against the dynamic type of the
expression <code>x</code>. As with type assertions, <code>x</code> must be of
<a href="#Interface_types">interface type</a>, and each non-interface type
<code>T</code> listed in a case must implement the type of <code>x</code>.
これは、「case
は実際の型T
を式x
の動的な型と照合する。型アサーションと同様に、x
はインターフェース型でなければならず、case
にリストされた各非インターフェース型T
はx
の型を実装していなければならない」という意味です。
この変更により、型スイッチのcase
で指定されるコンクリート型も、スイッチ対象のインターフェース型を実装している必要があることが明示されました。これにより、例えば interface{}
型の変数に対して、case MyStruct
のように、MyStruct
が interface{}
を実装していない(つまり、interface{}
は任意の型を受け入れるため、この制約は通常問題にならないが、より具体的なインターフェース型の場合に重要となる)ような無効なケースが仕様上も禁止されることになります。
select
ステートメントの変更点
select
ステートメントのセクションには、default
ケースに関する以下の記述が追加されました。
complete. There can be at most one default case and it may appear anywhere in the
"select" statement.
これは、「default
ケースは最大で1つしか存在できず、select
ステートメント内のどこにでも配置できる」ということを明確にしています。これはGoのselect
ステートメントの基本的なルールですが、このコミットで明示的に仕様に追記されました。
これらの変更は、Go言語の仕様をより厳密にし、コンパイラの振る舞いと仕様の間の整合性を高めることで、言語の定義をより堅牢なものにしています。
コアとなるコードの変更箇所
このコミットによるコアとなるコードの変更は、Go言語の仕様書である doc/go_spec.html
ファイルに対して行われています。
具体的には、以下のセクションが変更されています。
-
型アサーション (Type assertions) のセクション (
#Type_assertions
アンカー付近)x.(T)
のT
が非インターフェース型の場合の記述に、T
がx
のインターフェース型を実装している必要があるという制約が追加されました。- 関連するコード例が修正・追加されました。特に、無効な型アサーションの例 (
s := y.(string)
) が追加され、その理由がコメントで説明されています。 - 2値結果の型アサーションに関する記述のクリーンアップが行われました。
-
型スイッチ (Type switches) のセクション (
#Type_switches
アンカー付近)- 型スイッチの
case
にリストされる非インターフェース型T
が、スイッチ対象のインターフェース型を実装している必要があるという制約が追加されました。 - 型スイッチの例のコメントが修正され、各ケースにおける変数の型がより明確に示されました。
- 型スイッチの
-
select
ステートメント (Select statements) のセクション (#Select_statements
アンカー付近)default
ケースに関する記述に、「最大で1つしか存在できず、どこにでも配置できる」という情報が追加されました。
変更の差分 (diff) の抜粋:
--- a/doc/go_spec.html
+++ b/doc/go_spec.html
@@ -2668,8 +2668,11 @@ The notation <code>x.(T)</code> is called a <i>type assertion</i>.
More precisely, if <code>T</code> is not an interface type, <code>x.(T)</code> asserts
that the dynamic type of <code>x</code> is <a href="#Type_identity">identical</a>
to the type <code>T</code>.
+In this case, <code>T</code> must <a href="#Method_sets">implement</a> the (interface) type of <code>x</code>;
+otherwise the type assertion is invalid since it is not possible for <code>x</code>
+to store a value of type <code>T</code>.
If <code>T</code> is an interface type, <code>x.(T)</code> asserts that the dynamic type
-of <code>x</code> implements the interface <code>T</code> (§<a href="#Interface_types">Interface types</a>).
+of <code>x</code> implements the interface <code>T</code>.
</p>
<p>
If the type assertion holds, the value of the expression is the value
@@ -2679,8 +2682,19 @@ In other words, even though the dynamic type of <code>x</code>
is known only at run time, the type of <code>x.(T)</code> is
known to be <code>T</code> in a correct program.
</p>
+
+<pre>
+var x interface{} = 7 // x has dynamic type int and value 7
+i := x.(int) // i has type int and value 7
+
+type I interface { m() }\n+var y I
+s := y.(string) // illegal: string does not implement I (missing method m)
+r := y.(io.Reader) // r has type io.Reader and y must implement both I and io.Reader
+</pre>
+
<p>
-If a type assertion is used in an assignment or initialization of the form
+If a type assertion is used in an <a href="#Assignments">assignment</a> or initialization of the form
</p>
<pre>
@@ -2696,7 +2710,7 @@ otherwise, the expression returns <code>(Z, false)</code> where <code>Z</code>
is the <a href="#The_zero_value">zero value</a> for type <code>T</code>.\n No run-time panic occurs in this case.\n The type assertion in this construct thus acts like a function call\n-returning a value and a boolean indicating success. (§<a href="#Assignments">Assignments</a>)\n+returning a value and a boolean indicating success.\n </p>
@@ -4159,9 +4173,20 @@ case x == 4: f3()\n A type switch compares types rather than values. It is otherwise similar\n to an expression switch. It is marked by a special switch expression that\n has the form of a <a href="#Type_assertions">type assertion</a>\n-using the reserved word <code>type</code> rather than an actual type.\n-Cases then match literal types against the dynamic type of the expression\n-in the type assertion.\n+using the reserved word <code>type</code> rather than an actual type:\n+</p>\n+\n+<pre>\n+switch x.(type) {\n+// cases\n+}\n+</pre>\n+\n+<p>\n+Cases then match actual types <code>T</code> against the dynamic type of the\n+expression <code>x</code>. As with type assertions, <code>x</code> must be of\n+<a href="#Interface_types">interface type</a>, and each non-interface type\n+<code>T</code> listed in a case must implement the type of <code>x</code>.\n </p>
<pre class="ebnf">
@@ -4197,17 +4222,17 @@ the following type switch:\n <pre>\n switch i := x.(type) {\n case nil:\n-\tprintString(\"x is nil\")\n+\tprintString(\"x is nil\") // type of i is type of x (interface{})\n case int:\n-\tprintInt(i) // i is an int\n+\tprintInt(i) // type of i is int\n case float64:\n-\tprintFloat64(i) // i is a float64\n+\tprintFloat64(i) // type of i is float64\n case func(int) float64:\n-\tprintFunction(i) // i is a function\n+\tprintFunction(i) // type of i is func(int) float64\n case bool, string:\n-\tprintString(\"type is bool or string\") // i is an interface{}\n+\tprintString(\"type is bool or string\") // type of i is type of x (interface{})\n default:\n-\tprintString(\"don\'t know the type\")\n+\tprintString(\"don\'t know the type\") // type of i is type of x (interface{})\n }\n </pre>\
@@ -4218,22 +4243,23 @@ could be rewritten:\n <pre>\n v := x // x is evaluated exactly once\n if v == nil {\n+\ti := v // type of i is type of x (interface{})\n \tprintString(\"x is nil\")\n } else if i, isInt := v.(int); isInt {\n-\tprintInt(i) // i is an int\n+\tprintInt(i) // type of i is int\n } else if i, isFloat64 := v.(float64); isFloat64 {\n-\tprintFloat64(i) // i is a float64\n+\tprintFloat64(i) // type of i is float64\n } else if i, isFunc := v.(func(int) float64); isFunc {\n-\tprintFunction(i) // i is a function\n+\tprintFunction(i) // type of i is func(int) float64\n } else {\n-\ti1, isBool := v.(bool)\n-\ti2, isString := v.(string)\n+\t_, isBool := v.(bool)\n+\t_, isString := v.(string)\n \tif isBool || isString {\n-\t\ti := v\n-\t\tprintString(\"type is bool or string\") // i is an interface{}\n+\t\ti := v // type of i is type of x (interface{})\n+\t\tprintString(\"type is bool or string\")\n \t} else {\n-\t\ti := v\n-\t\tprintString(\"don\'t know the type\") // i is an interface{}\n+\t\ti := v // type of i is type of x (interface{})\n+\t\tprintString(\"don\'t know the type\")\n \t}\n }\n </pre>\
@@ -4501,7 +4527,8 @@ If any of the resulting operations can proceed, one of those is\n chosen and the corresponding communication and statements are\n evaluated. Otherwise, if there is a default case, that executes;\n if there is no default case, the statement blocks until one of the communications can\n-complete.\n+complete. There can be at most one default case and it may appear anywhere in the\n+"select" statement.\n If there are no cases with non-<code>nil</code> channels,\n the statement blocks forever.\n Even if the statement blocks,\n```
## コアとなるコードの解説
### 型アサーションの仕様変更
最も重要な変更は、型アサーション `x.(T)` において、`T` がインターフェース型ではない具体的な型である場合の制約の追加です。
変更前は、`x.(T)` が `x` の動的な型が `T` と同一であることをアサートするとだけ書かれていました。しかし、このコミットでは、以下の行が追加されました。
```html
In this case, <code>T</code> must <a href="#Method_sets">implement</a> the (interface) type of <code>x</code>;
otherwise the type assertion is invalid since it is not possible for <code>x</code>
to store a value of type <code>T</code>.
これは、x
が保持するインターフェース値が、アサートしようとしている具体的な型 T
の値を実際に格納できる場合にのみ、その型アサーションが有効であることを明確にしています。もし T
が x
のインターフェース型を実装していない場合、x
が T
型の値を保持することは論理的に不可能です。このようなアサーションはコンパイル時に無効と判断され、エラーとなります。
例として追加された s := y.(string)
のケースがこれをよく示しています。ここで y
は I
インターフェース型(m()
メソッドを持つ)であり、string
型は m()
メソッドを実装していません。したがって、y
が string
型の値を保持することはありえないため、この型アサーションは無効です。
この変更は、Goコンパイラ(gcおよびgccgo)が既にこのような無効なケースをエラーとして扱っていた「現状」を仕様に反映させたものであり、仕様と実装の整合性を高めるものです。
型スイッチの仕様変更
型スイッチ switch x.(type)
のセクションにも同様の制約が追加されました。
Cases then match actual types <code>T</code> against the dynamic type of the
expression <code>x</code>. As with type assertions, <code>x</code> must be of
<a href="#Interface_types">interface type</a>, and each non-interface type
<code>T</code> listed in a case must implement the type of <code>x</code>.
これは、型スイッチの case
で指定される具体的な型 T
も、スイッチ対象のインターフェース型 x
を実装していなければならないことを明示しています。型アサーションと同様に、これは論理的な制約であり、コンパイラは既にこのルールを適用していました。この追加により、仕様書がより正確で完全なものになりました。
型スイッチの例のコメントも更新され、各 case
ブロック内で宣言される変数 i
の型がより明確に示されています。例えば、case int:
の行では // type of i is int
と追記され、case bool, string:
や default:
の行では // type of i is type of x (interface{})
と追記されています。これにより、型スイッチのスコープ内で変数がどのように型付けされるかが一目でわかるようになりました。
select
ステートメントの default
ケースに関する追記
select
ステートメントのセクションには、default
ケースに関する以下の文が追加されました。
There can be at most one default case and it may appear anywhere in the
"select" statement.
これは、select
ステートメントにおいて default
ケースは最大で1つしか許されず、その位置は select
ブロック内のどこでも良いという、Go言語の基本的なルールを明文化したものです。これは既存の振る舞いを文書化したものであり、言語の定義をより厳密にするための軽微な改善です。
これらの変更は全体として、Go言語の型システムに関する仕様の曖昧さを解消し、コンパイラの振る舞いと仕様の間の整合性を高めることで、言語の堅牢性と理解しやすさを向上させています。
関連リンク
- Go言語の仕様書 (The Go Programming Language Specification): https://go.dev/ref/spec
- このコミットのGo Code Review (Gerrit): https://golang.org/cl/6869050
参考にした情報源リンク
- Go言語の仕様書 (The Go Programming Language Specification) - 型アサーション: https://go.dev/ref/spec#Type_assertions
- Go言語の仕様書 (The Go Programming Language Specification) - 型スイッチ: https://go.dev/ref/spec#Type_switches
- Go言語の仕様書 (The Go Programming Language Specification) - Select ステートメント: https://go.dev/ref/spec#Select_statements
- Go言語のインターフェースに関する公式ドキュメントやチュートリアル (一般的な知識として)
- Go言語の型アサーションと型スイッチに関する解説記事 (一般的な知識として)
- GitHubのコミットページ: https://github.com/golang/go/commit/485673188dc6c3ee3113990ed1e96ca8f8f0df51
- Gerrit Code Review: https://golang.org/cl/6869050 (コミットメッセージに記載されているリンク)
- Go言語のIssueトラッカー (過去のIssue検索の試み)