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

[インデックス 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がインターフェース型ではないコンクリート型であり、かつTxのインターフェース型を実装していない場合、あるいは型スイッチの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)oktrue の場合、アサーションは成功し、value には変換された値が格納されます。okfalse の場合、アサーションは失敗し、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が非インターフェース型の場合)、Txの(インターフェース)型を実装していなければならない。そうでなければ、xT型の値を格納することは不可能であるため、型アサーションは無効である」という意味です。

この追加により、例えば var y I; s := y.(string) のようなコードがなぜ無効であるかが明確になります。ここで Im() メソッドを持つインターフェースであり、string 型は m() メソッドを実装していないため、string はインターフェース I を実装していません。したがって、ystring 型の値を保持することは論理的に不可能です。この仕様の追加は、コンパイラが既にこのようなケースをエラーとして扱っていた振る舞いを仕様に反映させたものです。

また、型アサーションの例もクリーンアップされ、より明確な例が追加されました。

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にリストされた各非インターフェース型Txの型を実装していなければならない」という意味です。

この変更により、型スイッチのcaseで指定されるコンクリート型も、スイッチ対象のインターフェース型を実装している必要があることが明示されました。これにより、例えば interface{} 型の変数に対して、case MyStruct のように、MyStructinterface{} を実装していない(つまり、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 ファイルに対して行われています。

具体的には、以下のセクションが変更されています。

  1. 型アサーション (Type assertions) のセクション (#Type_assertions アンカー付近)

    • x.(T)T が非インターフェース型の場合の記述に、Tx のインターフェース型を実装している必要があるという制約が追加されました。
    • 関連するコード例が修正・追加されました。特に、無効な型アサーションの例 (s := y.(string)) が追加され、その理由がコメントで説明されています。
    • 2値結果の型アサーションに関する記述のクリーンアップが行われました。
  2. 型スイッチ (Type switches) のセクション (#Type_switches アンカー付近)

    • 型スイッチの case にリストされる非インターフェース型 T が、スイッチ対象のインターフェース型を実装している必要があるという制約が追加されました。
    • 型スイッチの例のコメントが修正され、各ケースにおける変数の型がより明確に示されました。
  3. 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 の値を実際に格納できる場合にのみ、その型アサーションが有効であることを明確にしています。もし Tx のインターフェース型を実装していない場合、xT 型の値を保持することは論理的に不可能です。このようなアサーションはコンパイル時に無効と判断され、エラーとなります。

例として追加された s := y.(string) のケースがこれをよく示しています。ここで yI インターフェース型(m() メソッドを持つ)であり、string 型は m() メソッドを実装していません。したがって、ystring 型の値を保持することはありえないため、この型アサーションは無効です。

この変更は、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言語の型システムに関する仕様の曖昧さを解消し、コンパイラの振る舞いと仕様の間の整合性を高めることで、言語の堅牢性と理解しやすさを向上させています。

関連リンク

参考にした情報源リンク