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

[インデックス 17423] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)におけるメソッド値の取り扱いに関するバグ修正です。具体的には、レシーバが名前のないインターフェース型である場合のメソッド値の生成が正しく行われない問題に対処しています。

コミット

commit a9e119ac7006c273d0045bcbc8c8d1a83f58f264
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Aug 29 10:00:58 2013 +0200

    cmd/gc: fix method values whose receiver is an unnamed interface.
    
    Fixes #6140.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/13083043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a9e119ac7006c273d0045bcbc8c8d1a83f58f264

元コミット内容

cmd/gc: fix method values whose receiver is an unnamed interface.

Fixes #6140.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/13083043

変更の背景

このコミットは、Goコンパイラが、レシーバが「名前のないインターフェース型」(unnamed interface type)であるメソッド値(method value)を誤って拒否するバグ(Issue 6140)を修正するために導入されました。

Go言語では、メソッドは特定の型に関連付けられた関数です。メソッド値は、レシーバが既にバインドされたメソッドを表現する関数のようなものです。例えば、myObject.MyMethod のように記述すると、MyMethodmyObject にバインドされた関数値として扱われます。

問題は、レシーバの型が明示的な名前を持たないインターフェース型である場合に発生しました。Goコンパイラの内部処理において、このような名前のないインターフェース型からメソッド値を作成しようとすると、コンパイラがレシーバのベース型を特定できず、不正なコード生成やコンパイルエラーを引き起こしていました。

具体的には、makepartialcall 関数内で、レシーバのシンボル(basetype->sym)がS(nilシンボル)である場合に致命的なエラーを発生させるロジックがありました。通常、名前付きの型にはシンボルが関連付けられていますが、名前のないインターフェース型の場合、そのシンボルがSとなることがあり、これが誤ってエラーとして扱われていました。

前提知識の解説

Go言語の型システム

Go言語の型システムは、静的型付けでありながら柔軟性を持っています。

  • 名前付き型 (Named Types): type MyInt int のように type キーワードで定義され、名前を持つ型です。
  • 無名型 (Unnamed Types): struct { X int; Y int }interface { m() } のように、type キーワードで明示的な名前を与えられずに直接定義される型です。これらは構造体リテラルやインターフェースリテラルとしてコード中に現れます。

インターフェース (Interfaces)

Goのインターフェースは、メソッドのシグネチャの集合を定義します。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たします。インターフェース型は、具体的な実装の詳細を抽象化し、ポリモーフィズムを実現するために使用されます。

メソッド値 (Method Values)

Goでは、メソッドを関数値として扱うことができます。これをメソッド値と呼びます。 例えば、type S struct{}; func (s S) M() {} という定義がある場合、s := S{} としたときに s.Mfunc() 型の関数値となります。この関数値は、レシーバ s が既にバインドされた状態で M メソッドを呼び出すことができます。

コンパイラは、メソッド値を生成する際に、レシーバの型情報から適切なメソッドを特定し、そのメソッドを呼び出すためのクロージャのようなコードを生成します。このプロセスは、コンパイラのバックエンド、特にcmd/gc(Goコンパイラのフロントエンドおよび中間コード生成部分)のclosure.cファイルで処理されます。

cmd/gcclosure.c

cmd/gc はGoコンパイラの主要部分であり、ソースコードの解析、型チェック、中間表現への変換、最適化、そして最終的な機械語コードの生成を行います。 closure.c は、クロージャ(匿名関数)やメソッド値の生成に関連するコードが含まれています。makepartialcall 関数は、メソッド値のような部分適用された関数呼び出しを生成する役割を担っています。

技術的詳細

この修正の核心は、makepartialcall 関数がレシーバのベース型を特定するロジックにあります。

修正前のコードでは、レシーバのベース型(basetype)がポインタ型であればその指す型に変換し、その後 basetype->sym == S (シンボルがnil)である場合に致命的なエラー fatal("missing base type for %T", rcvrtype) を発生させていました。このチェックは、通常、名前付きの型が期待される場所でシンボルが見つからない場合に、コンパイラが型情報を正しく取得できなかったことを示すためのものでした。

しかし、名前のないインターフェース型の場合、その型自体にはシンボルが関連付けられていません。インターフェース型は、そのメソッドセットによって定義されるため、シンボルは通常、そのインターフェースを実装する具体的な型に紐付けられます。したがって、名前のないインターフェース型がレシーバとして使用される場合、basetype->symS となるのは自然なことです。

修正は、この fatal エラーの条件を basetype->etype != TINTER && basetype->sym == S に変更しました。 これは、「ベース型がインターフェース型ではない」という条件を追加することで、インターフェース型の場合にはシンボルが S であってもエラーとしないようにしています。

さらに、メソッド値のパッケージ情報を決定するロジックも変更されました。 修正前は、sym = pkglookup(p, basetype->sym->pkg); のように、レシーバのベース型のシンボルからパッケージ情報を取得しようとしていました。しかし、名前のないインターフェース型の場合、basetype->sym->pkg が存在しないか、意味のない値になる可能性があります。

修正後は、以下のロジックが追加されました。

  1. spkg = nil; でパッケージシンボルを初期化。
  2. if(basetype->sym != S) spkg = basetype->sym->pkg; で、ベース型にシンボルがあればそのパッケージを取得。
  3. if(spkg == nil) { if(gopkg == nil) gopkg = mkpkg(strlit("go")); spkg = gopkg; } で、もしパッケージシンボルがまだnilであれば、デフォルトで "go" パッケージ(Goランタイムの内部パッケージ)を使用するようにしています。これは、名前のないインターフェース型のような特殊なケースで、適切なパッケージ情報が得られない場合にフォールバックとして機能します。

この変更により、コンパイラは名前のないインターフェース型をレシーバとするメソッド値に対しても、正しくパッケージ情報を特定し、コンパイルを進めることができるようになりました。

test/fixedbugs/issue6140.go は、このバグを再現し、修正が正しく機能することを確認するための新しいテストケースです。

  • type T *interface { m() int } のように、ポインタ型のレシーバを持つ名前のないインターフェース型を定義し、そのメソッド値 (*x).m を取得しようとしています。
  • var y interface { m() int } のように、直接名前のないインターフェース型を定義し、そのメソッド値 y.m を取得しようとしています。
  • var z *struct{ I } のように、埋め込みインターフェースを持つ構造体のポインタからメソッド値 z.String を取得しようとしています。

これらのケースは、修正前にはコンパイルエラーを引き起こしましたが、修正後は正しくコンパイルされるようになります。

test/method2.go は既存のテストファイルで、このコミットでは var _ = pv.val という行が追加されています。これは、ポインタ型のレシーバを持つメソッド値の取得に関する既存のテストに、今回の修正が影響を与えないことを確認するためのものです。

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

src/cmd/gc/closure.c

--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -285,6 +285,8 @@ makepartialcall(Node *fn, Type *t0, Node *meth)\n \tNodeList *body, *l, *callargs, *retargs;\n \tchar *p;\n \tSym *sym;\n+\tPkg *spkg;\n+\tstatic Pkg* gopkg;\n \tint i, ddd;\n \n \t// TODO: names are not right\n@@ -296,10 +298,18 @@ makepartialcall(Node *fn, Type *t0, Node *meth)\n \tbasetype = rcvrtype;\n \tif(isptr[rcvrtype->etype])\n \t\tbasetype = basetype->type;\n-\tif(basetype->sym == S)\n+\tif(basetype->etype != TINTER && basetype->sym == S)\n \t\tfatal(\"missing base type for %T\", rcvrtype);\n \n-\tsym = pkglookup(p, basetype->sym->pkg);\n+\tspkg = nil;\n+\tif(basetype->sym != S)\n+\t\tspkg = basetype->sym->pkg;\n+\tif(spkg == nil) {\n+\t\tif(gopkg == nil)\n+\t\t\tgopkg = mkpkg(strlit(\"go\"));\n+\t\tspkg = gopkg;\n+\t}\n+\tsym = pkglookup(p, spkg);\n \tfree(p);\n \tif(sym->flags & SymUniq)\n \t\treturn sym->def;\n```

### `test/fixedbugs/issue6140.go` (新規ファイル)

```diff
--- /dev/null
+++ b/test/fixedbugs/issue6140.go
@@ -0,0 +1,31 @@
+// compile
+
+// Copyright 2013 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.
+
+// Issue 6140: compiler incorrectly rejects method values
+// whose receiver has an unnamed interface type.
+
+package p
+
+type T *interface {
+	m() int
+}
+
+var x T
+
+var _ = (*x).m
+
+var y interface {
+	m() int
+}
+
+var _ = y.m
+
+type I interface {
+	String() string
+}
+
+var z *struct{ I }
+var _ = z.String

test/method2.go

--- a/test/method2.go
+++ b/test/method2.go
@@ -21,7 +21,7 @@ func (p *P1) val() int { return 1 } // ERROR \"receiver.* pointer|invalid pointer\n type I interface{}\n type I1 interface{}\n \n-func (p I) val() int { return 1 } // ERROR \"receiver.*interface|invalid pointer or interface receiver\"\n+func (p I) val() int   { return 1 } // ERROR \"receiver.*interface|invalid pointer or interface receiver\"\n func (p *I1) val() int { return 1 } // ERROR \"receiver.*interface|invalid pointer or interface receiver\"\n \n type Val interface {\n@@ -33,4 +33,5 @@ var _ = (*Val).val // ERROR \"method\"\n var v Val\n var pv = &v\n \n-var _ = pv.val()\t// ERROR \"method\"\n+var _ = pv.val() // ERROR \"method\"\n+var _ = pv.val   // ERROR \"method\"\n```

## コアとなるコードの解説

### `src/cmd/gc/closure.c` の変更点

1.  **`fatal` エラー条件の緩和**:
    ```c
    -	if(basetype->sym == S)
    +	if(basetype->etype != TINTER && basetype->sym == S)
     	fatal("missing base type for %T", rcvrtype);
    ```
    この変更により、`basetype` がインターフェース型(`TINTER`)である場合は、そのシンボルが `S`(nilシンボル)であっても致命的なエラーを発生させなくなりました。これにより、名前のないインターフェース型がレシーバとなるケースが正しく処理されるようになります。

2.  **パッケージシンボル取得ロジックの改善**:
    ```c
    -	sym = pkglookup(p, basetype->sym->pkg);
    +	Pkg *spkg;
    +	static Pkg* gopkg;
    +	// ...
    +	spkg = nil;
    +	if(basetype->sym != S)
    +		spkg = basetype->sym->pkg;
    +	if(spkg == nil) {
    +		if(gopkg == nil)
    +			gopkg = mkpkg(strlit("go"));
    +		spkg = gopkg;
    +	}
    +	sym = pkglookup(p, spkg);
    ```
    この部分では、メソッド値が属するパッケージを決定するためのロジックが変更されています。
    *   `spkg` という一時変数を導入し、レシーバのベース型にシンボルが存在する場合(`basetype->sym != S`)にそのパッケージ(`basetype->sym->pkg`)を `spkg` に代入します。
    *   もし `spkg` がまだ `nil` の場合(つまり、ベース型にシンボルがないか、シンボルがあってもパッケージ情報がない場合)、`gopkg` という静的変数に "go" パッケージの情報を格納し、それを `spkg` に設定します。これは、名前のないインターフェース型のような、明示的なパッケージに属さない可能性のある型に対するフォールバックメカニズムです。
    *   最終的に、`pkglookup` 関数にこの `spkg` を渡すことで、メソッド値のシンボルが正しいパッケージに関連付けられるようにします。

### `test/fixedbugs/issue6140.go` の追加

このファイルは、今回のバグ修正を検証するための新しいテストケースです。
*   `type T *interface { m() int }` は、ポインタ型のレシーバを持つ無名インターフェース型を定義しています。
*   `var _ = (*x).m` は、この無名インターフェース型のポインタからメソッド値を取得しようとしています。
*   `var y interface { m() int }` は、直接無名インターフェース型を定義しています。
*   `var _ = y.m` は、この無名インターフェース型からメソッド値を取得しようとしています。
*   `var z *struct{ I }` と `var _ = z.String` は、埋め込みインターフェースを持つ構造体のポインタからメソッド値を取得するケースをテストしています。

これらのケースは、修正前はコンパイルエラーになりましたが、修正後は正しくコンパイルされることを確認します。

### `test/method2.go` の変更

既存のテストファイルに `var _ = pv.val` という行が追加されました。これは、ポインタ型のレシーバを持つメソッド値の取得に関する既存のテストスイートに、今回の修正が意図しない副作用をもたらさないことを確認するためのものです。

これらの変更により、Goコンパイラは、レシーバが名前のないインターフェース型である場合でも、メソッド値を正しく生成できるようになり、Go言語の柔軟な型システムをより完全にサポートするようになりました。

## 関連リンク

*   Go Issue 6140: [https://github.com/golang/go/issues/6140](https://github.com/golang/go/issues/6140)
*   Go Code Review: [https://golang.org/cl/13083043](https://golang.org/cl/13083043)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント (特に型システム、インターフェース、メソッドに関するセクション)
*   Goコンパイラのソースコード (`src/cmd/gc/closure.c`)
*   GoのIssueトラッカー (Issue 6140の詳細)
*   Goのコードレビューシステム (CL 13083043)
*   Go言語のメソッド値に関する一般的な解説記事
*   Go言語の無名型に関する解説記事
*   Go言語のコンパイラ内部構造に関する資料 (もしあれば)