[インデックス 1567] ファイルの概要
このコミットは、Go言語のコンパイラにおけるバグ修正に関するものです。具体的には、インターフェース型のアサーション f, ok := i.(Foo)
が、i
が既に Foo
と同等である場合にコンパイルエラーとなる問題を解決しています。この修正は、Go言語の初期段階における型システムとコンパイラの挙動の成熟を示す重要な一歩と言えます。
コミット
commit fa615a3b303fdf10e9e3dcd21d372c0ed8e7351a
Author: Rob Pike <r@golang.org>
Date: Mon Jan 26 18:35:18 2009 -0800
f, ok := i.(Foo) does not compile if i already is equivalent to Foo
R=rsc
DELTA=18 (18 added, 0 deleted, 0 changed)
OCL=23544
CL=23547
---
test/bugs/bug135.go | 18 ++++++++++++++++++
test/golden.out | 4 ++++\
2 files changed, 22 insertions(+)
diff --git a/test/bugs/bug135.go b/test/bugs/bug135.go
new file mode 100644
index 0000000000..d7115c4f27
--- /dev/null
+++ b/test/bugs/bug135.go
@@ -0,0 +1,18 @@
+// $G $D/$F.go || echo BUG: should compile
+//
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+//
+package main
+//
+type Foo interface { }
+//
+type T struct {}
+func (t *T) foo() {}
+//
+func main() {
+// t := new(T);
+// var i interface {};
+// f, ok := i.(Foo);
+// }
diff --git a/test/golden.out b/test/golden.out
index d70df181d3..241225ab09 100644
--- a/test/golden.out
+++ b/test/golden.out
@@ -181,6 +181,10 @@ BUG: should not compile
=========== bugs/bug132.go
BUG: compilation succeeds incorrectly
+=========== bugs/bug135.go
+bugs/bug135.go:13: assignment count mismatch: 2 = 1
+BUG: should compile
+
=========== fixedbugs/bug016.go
fixedbugs/bug016.go:7: overflow converting constant to uint
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fa615a3b303fdf10e9e3dcd21d372c0ed8e7351a
元コミット内容
f, ok := i.(Foo) does not compile if i already is equivalent to Foo
このコミットメッセージは、Go言語の型アサーションに関する特定のバグを簡潔に示しています。i.(Foo)
という型アサーションが、変数 i
が既に Foo
インターフェースと同等である場合にコンパイルエラーになるという問題です。
変更の背景
Go言語は静的型付け言語でありながら、インターフェースを通じて動的な振る舞いを許容します。型アサーションは、インターフェース型の変数が実際に特定の具象型または別のインターフェース型を保持しているかどうかを確認し、その値を取り出すための重要なメカニズムです。
このコミットが作成された2009年1月は、Go言語がまだ一般に公開される前の開発初期段階でした。当時のGoコンパイラはまだ成熟しておらず、型システムに関するエッジケースやバグが多数存在していました。このバグは、インターフェースの内部表現と型アサーションの処理ロジックにおける不整合に起因するものと考えられます。
具体的には、f, ok := i.(Foo)
という形式の型アサーションは、i
が Foo
インターフェースを実装しているかどうかをチェックし、もし実装していればその値を f
に代入し、成功したかどうかを ok
にブール値で返します。このバグは、i
が既に Foo
インターフェース型である場合に、コンパイラがこのアサーションを正しく処理できず、「assignment count mismatch: 2 = 1」のようなエラーを吐き出していたことを示唆しています。これは、コンパイラが i.(Foo)
の結果を単一の値として解釈し、2つの変数(f
と ok
)への代入とミスマッチを起こしていた可能性が高いです。
前提知識の解説
このコミットの理解には、以下のGo言語の概念が不可欠です。
-
インターフェース (Interfaces): Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、JavaやC#のような明示的な
implements
キーワードを必要とせず、型がインターフェースで定義されたすべてのメソッドを実装していれば、そのインターフェースを満たしていると見なされます(構造的型付け)。 例:type Reader interface { Read(p []byte) (n int, err error) }
interface{}
は「空のインターフェース」と呼ばれ、いかなる型もメソッドを持たないため、すべての型がinterface{}
を満たします。これは、任意の型の値を保持できる汎用的なコンテナとしてよく使用されます。 -
型アサーション (Type Assertions): 型アサーションは、インターフェース型の変数が保持している基になる具象型、または別のインターフェース型を動的にチェックし、その値を取り出すための構文です。 構文は
i.(T)
の形式を取ります。ここでi
はインターフェース型の変数、T
はチェックしたい型です。型アサーションには2つの形式があります。
- 単一の戻り値:
value := i.(T)
この形式は、i
がT
型を保持していない場合にパニック(ランタイムエラー)を引き起こします。 - 2つの戻り値("comma ok" idiom):
value, ok := i.(T)
この形式は、i
がT
型を保持しているかどうかを安全にチェックします。ok
はブール値で、アサーションが成功した場合はtrue
、失敗した場合はfalse
となります。成功した場合、value
にはT
型に変換された値が代入されます。失敗した場合、value
にはT
型のゼロ値が代入されます。この形式は、パニックを避けるために推奨されます。
このコミットで問題となっているのは、まさにこの2つの戻り値を持つ型アサーションの形式です。
- 単一の戻り値:
-
new(T)
:new(T)
は、型T
のゼロ値に初期化された新しい項目を割り当て、その項目へのポインタを返します。このコミットのテストコードではt := new(T)
が使用されています。
技術的詳細
このバグの核心は、Goコンパイラが型アサーション i.(Foo)
の結果をどのように処理していたかにあります。通常、i.(Foo)
は、成功時には Foo
型の値と true
のブール値の2つの値を返します。しかし、バグのあるコンパイラは、i
が既に Foo
インターフェース型であるという特殊なケースにおいて、このアサーションの結果を単一の値として誤って解釈していたと考えられます。
Go言語のインターフェースは、内部的には「型」と「値」のペアとして表現されます。interface{}
型の変数 i
が Foo
インターフェース型の値を保持している場合、その「型」の部分は Foo
インターフェース型であり、「値」の部分は Foo
インターフェースを実装する具象型の値です。
f, ok := i.(Foo)
という型アサーションは、コンパイラにとって以下のようなロジックを生成する必要があります。
i
が保持する動的な型がFoo
インターフェースを満たしているかチェックする。- もし満たしていれば、
i
が保持する動的な値をFoo
インターフェース型としてf
に代入し、ok
にtrue
を代入する。 - 満たしていなければ、
f
にFoo
インターフェース型のゼロ値(通常はnil
)を代入し、ok
にfalse
を代入する。
バグのあるコンパイラは、i
が既に Foo
インターフェース型であるという状況で、ステップ1のチェックが不要であるか、あるいは異なるパスを通るべきだと誤解釈し、結果として単一の値を生成するコードを吐き出してしまった可能性があります。これにより、f, ok
という2つの変数への代入と、i.(Foo)
からの単一の値の出力との間で「assignment count mismatch: 2 = 1」というエラーが発生していました。
この修正は、コンパイラの型アサーション処理ロジック、特にインターフェースからインターフェースへのアサーションのケースにおいて、生成されるコードが常に2つの値を返すように調整されたことを示唆しています。これにより、Go言語の型システムの一貫性と堅牢性が向上しました。
コアとなるコードの変更箇所
このコミットでは、Go言語のコンパイラ自体のコードは直接変更されていません。代わりに、このバグを再現し、修正が適用されたことを検証するためのテストケースが追加されています。
-
test/bugs/bug135.go
の追加: このファイルは、問題の型アサーションf, ok := i.(Foo)
を含む新しいテストケースです。package main type Foo interface { } type T struct {} func (t *T) foo() {} func main() { t := new(T); var i interface {}; f, ok := i.(Foo); // この行が問題の箇所 }
このテストファイルの冒頭には
// $G $D/$F.go || echo BUG: should compile
というコメントがあります。これは、Goコンパイラ ($G
) でこのファイルをコンパイル ($D/$F.go
) した際に、エラーが発生するはずだが、最終的にはコンパイルが成功するべきである(BUG: should compile
)という意図を示しています。つまり、このテストは、バグが修正される前はコンパイルエラーになるが、修正後は成功することを確認するためのものです。 -
test/golden.out
の変更:test/golden.out
は、Go言語のテストスイートにおける期待される出力(ゴールデンファイル)を記録するファイルです。このファイルに、bug135.go
のテスト結果が追加されています。=========== bugs/bug135.go bugs/bug135.go:13: assignment count mismatch: 2 = 1 BUG: should compile
このエントリは、バグ修正前のコンパイラが
bug135.go
の13行目で「assignment count mismatch: 2 = 1」というエラーを報告していたことを示しています。そして、そのエラーが「BUG: should compile」である、つまり、本来はコンパイルが成功すべきであったことを明記しています。修正後は、このエラーメッセージは表示されなくなり、テストは成功するようになります。
コアとなるコードの解説
追加された bug135.go
のコードは非常にシンプルですが、Go言語の型システムにおける重要なエッジケースを突いています。
package main
type Foo interface { } // 空のインターフェースFooを定義
type T struct {}
func (t *T) foo() {} // T型にメソッドfoo()を定義(このメソッドはFooインターフェースとは無関係)
func main() {
t := new(T); // T型のポインタを作成
var i interface {}; // 空のインターフェース型の変数iを宣言
// ここでiはnilであり、何の具象値も保持していない
f, ok := i.(Foo); // iがFooインターフェースを実装しているかチェック
}
このコードのポイントは、var i interface {};
の宣言直後、i
は nil
値と nil
型を保持しているという点です。Goのインターフェースは、具象値と具象型が両方とも nil
でない限り nil
ではありません。この場合、i
は nil
です。
f, ok := i.(Foo)
という型アサーションは、i
が Foo
インターフェースを実装しているかどうかをチェックします。i
は現在 nil
なので、このアサーションは失敗し、f
には Foo
インターフェースのゼロ値(nil
)、ok
には false
が代入されることが期待されます。
しかし、バグのあるコンパイラは、この i.(Foo)
の部分を処理する際に、i
が interface{}
型であり、Foo
もインターフェース型であるという事実から、何らかの内部的な誤解釈を生じさせていました。特に、Foo
が空のインターフェースであるため、すべての型が Foo
を満たすという特性が、コンパイラのロジックを複雑にしていた可能性があります。
このバグは、コンパイラがインターフェースからインターフェースへの型アサーション、特にソースインターフェースが空のインターフェースである場合や、ターゲットインターフェースも空のインターフェースである場合に、生成すべきコードの数を誤っていたことを示しています。修正により、コンパイラはこのようなケースでも常に2つの戻り値(値とok
ブール値)を正しく生成するようになりました。
関連リンク
- Go言語のインターフェースに関する公式ドキュメント: https://go.dev/tour/methods/10
- Go言語の型アサーションに関する公式ドキュメント: https://go.dev/tour/methods/15
- Go言語の初期のバグトラッカーやメーリングリストのアーカイブ(もし公開されていれば、この特定のバグに関する議論が見つかる可能性がありますが、2009年のものは見つけにくいかもしれません。)
参考にした情報源リンク
- Go言語の公式ドキュメント (Go Tour)
- Go言語のソースコードリポジトリ (GitHub)
- Go言語の型システムに関する一般的な知識
- Go言語のコンパイラの内部構造に関する一般的な知識 (Goのコンパイラは、型チェックとコード生成の段階で、このような型アサーションをどのように処理するかを決定します。)
- Go言語の初期のコミット履歴とテストスイートの構造。
- Go言語の
interface{}
と型アサーションに関する一般的な解説記事。 - Go言語の
new
とmake
の違いに関する解説。 - Go言語の
nil
インターフェースに関する解説。