[インデックス 1810] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)において、特定の型(T)がポインタレシーバを持つメソッドを定義しているにもかかわらず、そのTの非ポインタ型をインターフェースに代入しようとした際に、コンパイラが警告を発するようにする変更です。これは一時的なヒューリスティックな修正であり、より広範なインターフェース関連の問題への対処を保留しつつ、Robertという開発者が直面した具体的な問題を解決するために導入されました。また、pretty
パッケージにおける軽微なバグ修正も含まれています。
コミット
- コミットハッシュ:
4eb7ceba58780b654c7411c6be593aaf8f23a455
- Author: Russ Cox rsc@golang.org
- Date: Wed Mar 11 16:06:17 2009 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4eb7ceba58780b654c7411c6be593aaf8f23a455
元コミット内容
complain when trying to put T into an interface
if T has pointer methods. this is just a heuristic
but it catches the problem robert ran into and
lets me put the larger interface issues aside for
now. found one bug in pretty.
R=ken
OCL=26141
CL=26141
変更の背景
Go言語の初期段階において、インターフェースとメソッドの関連付けに関するセマンティクスはまだ進化途上にありました。特に、値レシーバとポインタレシーバを持つメソッドがインターフェースの要件を満たすかどうかのルールは、開発者にとって混乱を招くことがありました。
このコミットの背景には、Robert
という開発者が遭遇した具体的な問題があります。これは、構造体などの具象型がポインタレシーバを持つメソッド(例: func (p *MyStruct) MyMethod()
) を定義しているにもかかわらず、その構造体の値(例: var s MyStruct
)を、そのメソッドを要求するインターフェース型に代入しようとした際に発生する問題です。
Goの仕様では、インターフェースのメソッドセットは、具象型のメソッドセットによって満たされます。ここで重要なのは、値レシーバのメソッドは値とポインタの両方で呼び出せるのに対し、ポインタレシーバのメソッドはポインタでのみ呼び出せるという点です。したがって、具象型がポインタレシーバのメソッドしか持たない場合、その型の値をインターフェースに代入すると、インターフェースのメソッドセットが満たされないという論理的な不整合が生じます。
このコミットは、この問題に対する「一時的なヒューリスティックな」解決策として導入されました。つまり、より根本的なインターフェースのセマンティクスに関する議論や変更を待つ間、開発者がよく陥るこの種のバグを早期に検出するための警告メカニズムとして機能します。また、usr/gri/pretty/parser.go
における軽微なフォーマットのバグも同時に修正されています。
前提知識の解説
このコミットを理解するためには、Go言語における以下の概念を理解しておく必要があります。
-
インターフェース (Interfaces): Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。具象型がインターフェースのすべてのメソッドを実装していれば、その具象型は自動的にそのインターフェースを満たします(暗黙的な実装)。
-
メソッドセット (Method Sets): Goの型には「メソッドセット」という概念があります。これは、その型が持つメソッドの集合です。
- 値型
T
のメソッドセット:T
のメソッドセットには、値レシーバ (func (t T) Method()
) を持つすべてのメソッドが含まれます。 - ポインタ型
*T
のメソッドセット:*T
のメソッドセットには、値レシーバ (func (t T) Method()
) とポインタレシーバ (func (t *T) Method()
) の両方を持つすべてのメソッドが含まれます。
- 値型
-
インターフェースの満足 (Interface Satisfaction): 具象型
T
がインターフェースI
を満たすのは、T
のメソッドセットがI
のメソッドセットのすべてのメソッドを含む場合です。 同様に、具象型*T
がインターフェースI
を満たすのは、*T
のメソッドセットがI
のメソッドセットのすべてのメソッドを含む場合です。ここで重要なのは、もしインターフェース
I
がポインタレシーバのメソッドを要求している場合、値型T
はそのインターフェースを満たすことができません。なぜなら、T
のメソッドセットにはポインタレシーバのメソッドが含まれないからです。しかし、ポインタ型*T
はそのインターフェースを満たすことができます。例:
type MyInterface interface { Update() // ポインタレシーバを想定する操作 } type MyStruct struct { value int } func (s *MyStruct) Update() { // ポインタレシーバのメソッド s.value++ } func main() { var s MyStruct // 値型 // var i MyInterface = s // これはコンパイルエラーになるべき // なぜなら MyStruct のメソッドセットには Update() が含まれないため // MyStruct のメソッドセットは空。*MyStruct のメソッドセットには Update() が含まれる。 var ps *MyStruct = &s // ポインタ型 var i MyInterface = ps // これはOK }
このコミット以前は、上記のようなケースでコンパイラが適切なエラーを出さず、実行時エラーや予期せぬ動作につながる可能性がありました。
技術的詳細
このコミットの主要な変更は、Goコンパイラの型チェック部分、具体的には src/cmd/gc/subr.c
ファイル内の ifaceokT2I
関数にあります。この関数は、具象型 t0
がインターフェース型 iface
を満たすかどうかをチェックする役割を担っています。
変更の核心は、以下の新しいコードブロックの追加です。
// stopgap: check for
// non-pointer type in T2I, methods want pointers.
// supposed to do something better eventually
// but this will catch errors while we decide the
// details of the "better" solution.
if(t == t0 && t->methptr == 2) {
yyerror("probably wanted *%T not %T", t, t);
*m = iface->type;
return 0;
}
このコードは、以下の条件をチェックしています。
t == t0
: これは、ifaceokT2I
関数に渡された具象型t0
が、methtype(t0)
の結果であるt
と同じであるかどうかを確認しています。methtype
関数は、型のメソッドセットを決定するために使用されます。この条件が真であるということは、t0
がポインタ型ではない(つまり、値型である)ことを意味します。もしt0
がポインタ型であれば、methtype
はそのポインタが指す基底型を返すか、あるいはポインタ型自体のメソッドセットを考慮した結果を返すため、t
とt0
が異なる可能性があります。t->methptr == 2
: ここがこのヒューリスティックの肝です。t->methptr
は、Goコンパイラの内部で型t
が持つメソッドの種類に関する情報を格納するフィールドであると推測されます。methptr == 0
: メソッドなし、または値レシーバのメソッドのみ。methptr == 1
: ポインタレシーバのメソッドのみ。methptr == 2
: 値レシーバとポインタレシーバの両方のメソッドを持つ、またはポインタレシーバのメソッドを持つが、その型が値型である場合に特別な処理が必要なケース。 このコンテキストでは、t->methptr == 2
は、型t
がポインタレシーバを持つメソッドを定義していることを示唆していると考えられます。
これらの条件が両方とも真である場合(つまり、値型 t0
がポインタレシーバを持つメソッドを定義しているにもかかわらず、その値型をインターフェースに代入しようとしている場合)、コンパイラは yyerror
を呼び出して以下の警告メッセージを出力します。
"probably wanted *%T not %T"
これは、「おそらく %T
ではなく *%T
を意図していました」という意味で、開発者に対して、値型ではなくそのポインタ型をインターフェースに代入すべきだったことを示唆します。これにより、コンパイル時に潜在的な実行時エラーを防ぐことができます。
この修正は「stopgap」(一時しのぎ)と明記されており、将来的にはより洗練された解決策が導入されることを示唆しています。しかし、当時のGo言語のインターフェースセマンティクスに関する議論が進行中であったことを考えると、これは実用的なアプローチでした。
test/interface4.go
と test/interface6.go
の変更は、この新しい警告をテストするために、以前はコンパイルが通っていたが、この変更によって警告が出るようになるコード行をコメントアウトしています。これにより、コンパイラの新しい動作が意図通りであることを確認しています。
usr/gri/pretty/parser.go
の変更は、コードのフォーマットに関する軽微な修正であり、このコミットの主要な目的とは直接関係ありませんが、同時に修正されました。
コアとなるコードの変更箇所
src/cmd/gc/subr.c
の ifaceokT2I
関数に以下のコードが追加されました。
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2805,12 +2805,23 @@ ifacelookdot(Sym *s, Type *t)
// check whether non-interface type t
// satisifes inteface type iface.
int
-ifaceokT2I(Type *t, Type *iface, Type **m)
+ifaceokT2I(Type *t0, Type *iface, Type **m)
{
-\tType *im, *tm;\n+\tType *t, *im, *tm;
\tint imhash;
\n-\tt = methtype(t);\n+\tt = methtype(t0);
+\n+\t// stopgap: check for
+\t// non-pointer type in T2I, methods want pointers.
+\t// supposed to do something better eventually
+\t// but this will catch errors while we decide the
+\t// details of the "better" solution.
+\tif(t == t0 && t->methptr == 2) {
+\t\tyyerror("probably wanted *%T not %T", t, t);
+\t\t*m = iface->type;
+\t\treturn 0;
+\t}
\n \t// if this is too slow,\n \t// could sort these first
コアとなるコードの解説
ifaceokT2I
関数は、Goコンパイラのバックエンドの一部であり、具象型がインターフェースを満たすかどうかの型チェックロジックを実装しています。
変更前は、この関数は単に methtype(t)
を呼び出して型のメソッドセットを取得し、インターフェースのメソッドセットと比較していました。しかし、Goのメソッドセットのルール(値レシーバのメソッドは値とポインタの両方で呼び出せるが、ポインタレシーバのメソッドはポインタでのみ呼び出せる)により、具象型の値がポインタレシーバのメソッドを持つインターフェースを満たさないという微妙なケースがありました。
追加されたコードは、この特定のケースを捕捉するためのものです。
Type *t0
: インターフェースに代入しようとしている元の具象型。Type *t
:methtype(t0)
の結果。これはt0
のメソッドセットを決定するために使用される型です。t == t0
: この条件は、t0
がポインタ型ではない(つまり、値型である)ことを確認します。もしt0
が*MyStruct
のようなポインタ型であれば、methtype
はMyStruct
を返す可能性があり、その場合t != t0
となります。t->methptr == 2
: この内部フラグは、型t
がポインタレシーバを持つメソッドを定義していることを示します。
したがって、t == t0 && t->methptr == 2
という条件は、「インターフェースに代入しようとしているのが値型であり、かつその値型がポインタレシーバを持つメソッドを定義している」という状況を正確に特定します。
この条件が満たされた場合、yyerror
を使ってコンパイルエラーメッセージを出力します。yyerror
はGoコンパイラがエラーや警告を報告するために使用する関数です。メッセージ "probably wanted *%T not %T"
は、開発者に対して、値型ではなくポインタ型を使用すべきだったことを明確に伝えます。
*m = iface->type;
と return 0;
は、インターフェースの満足チェックが失敗したことを示し、コンパイラがそれ以上の処理を停止するようにします。
この変更は、Go言語の型システムがまだ初期段階にあった頃の、実用的な問題解決のためのアプローチを示しています。コンパイラがより賢くなり、開発者が陥りやすい落とし穴を早期に検出できるようになることで、Goコードの堅牢性が向上しました。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやチュートリアルは、この概念を深く理解するのに役立ちます。
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/subr.c
の関連部分) - Go言語の公式ドキュメントおよび仕様書
- Go言語の初期のコミット履歴と関連する議論 (GoのメーリングリストやIssueトラッカーなど)
- Go言語における値レシーバとポインタレシーバに関する一般的な解説記事