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

[インデックス 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言語における以下の概念を理解しておく必要があります。

  1. インターフェース (Interfaces): Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。具象型がインターフェースのすべてのメソッドを実装していれば、その具象型は自動的にそのインターフェースを満たします(暗黙的な実装)。

  2. メソッドセット (Method Sets): Goの型には「メソッドセット」という概念があります。これは、その型が持つメソッドの集合です。

    • 値型 T のメソッドセット: T のメソッドセットには、値レシーバ (func (t T) Method()) を持つすべてのメソッドが含まれます。
    • ポインタ型 *T のメソッドセット: *T のメソッドセットには、値レシーバ (func (t T) Method()) とポインタレシーバ (func (t *T) Method()) の両方を持つすべてのメソッドが含まれます。
  3. インターフェースの満足 (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;
	}

このコードは、以下の条件をチェックしています。

  1. t == t0: これは、ifaceokT2I 関数に渡された具象型 t0 が、methtype(t0) の結果である t と同じであるかどうかを確認しています。methtype 関数は、型のメソッドセットを決定するために使用されます。この条件が真であるということは、t0 がポインタ型ではない(つまり、値型である)ことを意味します。もし t0 がポインタ型であれば、methtype はそのポインタが指す基底型を返すか、あるいはポインタ型自体のメソッドセットを考慮した結果を返すため、tt0 が異なる可能性があります。
  2. 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.gotest/interface6.go の変更は、この新しい警告をテストするために、以前はコンパイルが通っていたが、この変更によって警告が出るようになるコード行をコメントアウトしています。これにより、コンパイラの新しい動作が意図通りであることを確認しています。

usr/gri/pretty/parser.go の変更は、コードのフォーマットに関する軽微な修正であり、このコミットの主要な目的とは直接関係ありませんが、同時に修正されました。

コアとなるコードの変更箇所

src/cmd/gc/subr.cifaceokT2I 関数に以下のコードが追加されました。

--- 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 のようなポインタ型であれば、methtypeMyStruct を返す可能性があり、その場合 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言語のソースコード (特に src/cmd/gc/subr.c の関連部分)
  • Go言語の公式ドキュメントおよび仕様書
  • Go言語の初期のコミット履歴と関連する議論 (GoのメーリングリストやIssueトラッカーなど)
  • Go言語における値レシーバとポインタレシーバに関する一般的な解説記事