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

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

このコミットは、Goコンパイラ(cmd/gc)における、同じ名前の非エクスポートメソッドが混同されるバグを修正するものです。具体的には、異なるパッケージに属する型が、同じ名前の非エクスポートメソッドを持つ場合に、コンパイラがそれらを正しく区別できない問題に対処しています。

コミット

commit 987a580b9f91dfe0709d6927525952acf9101fc9
Author: Russ Cox <rsc@golang.org>
Date:   Wed Mar 7 01:55:17 2012 -0500

    cmd/gc: do not confuse unexported methods of same name
    
    Fixes #3146.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5756074

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

https://github.com/golang/go/commit/987a580b9f91dfe0709d6927525952acf9101fc9

元コミット内容

このコミットは、Goコンパイラが、異なるパッケージに存在する同じ名前の非エクスポートメソッドを誤って同一視してしまう問題を解決します。この問題は、特にインターフェースの埋め込みや型のアサーションにおいて、予期せぬメソッド呼び出しやコンパイルエラーを引き起こす可能性がありました。

変更の背景

このコミットは、Go issue #3146「gc: unexported methods of same name confuse interface embedding」を修正するために作成されました。このバグは、異なるパッケージで定義された型が、たまたま同じ名前の非エクスポートメソッドを持っている場合に発生しました。Goのコンパイラは、メソッドのシンボル名を生成する際に、非エクスポートメソッドの場合にパッケージ情報を十分に考慮していなかったため、これらのメソッドを区別できず、結果として誤ったメソッドが解決されたり、コンパイルエラーが発生したりしていました。

具体的には、test/bugs/424.go(後にtest/fixedbugs/bug424.goに移動)で示されたテストケースがこの問題を浮き彫りにしました。このテストケースでは、libパッケージとmainパッケージの両方にm()という非エクスポートメソッドを持つ型が存在し、インターフェースの埋め込みを通じてこれらのメソッドがどのように解決されるかを検証していました。バグが存在する状態では、mainパッケージのmyT3型がlibパッケージのm()メソッドを誤って解決してしまうという問題が発生していました。

前提知識の解説

Go言語におけるエクスポートと非エクスポート

Go言語では、識別子(変数、関数、型、メソッドなど)の可視性は、その名前の最初の文字が大文字か小文字かによって決まります。

  • エクスポートされた識別子 (Exported Identifiers): 最初の文字が大文字の場合、その識別子はパッケージの外部からアクセス可能です。これは他の言語のpublicに相当します。
  • 非エクスポートされた識別子 (Unexported Identifiers): 最初の文字が小文字の場合、その識別子は定義されたパッケージ内からのみアクセス可能です。これは他の言語のprivateinternalに相当します。

メソッドの場合も同様で、メソッド名が小文字で始まる場合、そのメソッドは定義されたパッケージ内でのみ呼び出すことができます。

Goコンパイラのシンボル生成

Goコンパイラは、プログラム内の様々な要素(変数、関数、型、メソッドなど)を一意に識別するために「シンボル」を生成します。特にメソッドの場合、シンボルはレシーバの型とメソッド名を組み合わせて生成されることが一般的です。しかし、非エクスポートメソッドの場合、同じ名前のメソッドが異なるパッケージに存在すると、コンパイラがそれらを区別するための追加情報(パッケージ名など)が必要になります。

src/cmd/gc/dcl.cmethodsym関数

src/cmd/gc/dcl.cは、Goコンパイラのフロントエンドの一部であり、宣言(declaration)の処理を担当しています。このファイルには、型、変数、関数、メソッドなどの宣言を解析し、それらのシンボルを生成するロジックが含まれています。

methodsym関数は、特定の型に属するメソッドのシンボル名を生成する役割を担っています。この関数は、メソッドのレシーバ型(t0)とメソッド名(nsym)を受け取り、それらを組み合わせて一意のシンボル名を構築します。このシンボル名は、コンパイラの内部でメソッドを一意に識別するために使用されます。

バグの発生前は、methodsym関数が非エクスポートメソッドのシンボル名を生成する際に、メソッドが属するパッケージの情報を十分に含めていませんでした。そのため、異なるパッケージに同じ名前の非エクスポートメソッドが存在すると、生成されるシンボル名が衝突し、コンパイラがそれらを区別できなくなっていました。

技術的詳細

このコミットの核心は、src/cmd/gc/dcl.c内のmethodsym関数の変更にあります。この関数は、Goコンパイラがメソッドの内部シンボル名を生成する際に使用されます。

変更前は、methodsym関数はメソッドのレシーバ型とメソッド名に基づいてシンボル名を生成していました。非エクスポートメソッドの場合、この生成ロジックは、メソッドが属するパッケージを考慮していませんでした。

// 変更前の関連コードスニペット
if(t0->sym == S && isptr[t0->etype])
    p = smprint("(%-hT).%s%s", t0, nsym->name, suffix);
else
    p = smprint("%-hT.%s%s", t0, nsym->name, suffix);

このコードでは、nsym->name(メソッド名)とt0(レシーバ型)のみを使用してシンボル名を構築しています。suffixは、インターフェースメソッドの場合に追加される接尾辞です。非エクスポートメソッドの場合、nsym->nameは小文字で始まりますが、これだけでは異なるパッケージの同じ名前のメソッドを区別できません。

このコミットでは、以下の条件が追加されました。

// 変更後の関連コードスニペット
if(nsym->pkg != s->pkg && !exportname(nsym->name)) {
    if(t0->sym == S && isptr[t0->etype])
        p = smprint("(%-hT).%s.%s%s", t0, nsym->pkg->prefix, nsym->name, suffix);
    else
        p = smprint("%-hT.%s.%s%s", t0, nsym->pkg->prefix, nsym->name, suffix);
} else {
    // 既存のロジック
    if(t0->sym == S && isptr[t0->etype])
        p = smprint("(%-hT).%s%s", t0, nsym->name, suffix);
    else
        p = smprint("%-hT.%s%s", t0, nsym->name, suffix);
}

この変更のポイントは、if(nsym->pkg != s->pkg && !exportname(nsym->name))という条件です。

  • nsym->pkg != s->pkg: これは、メソッドが定義されているパッケージ(nsym->pkg)が、現在のシンボルが属するパッケージ(s->pkg)と異なるかどうかをチェックします。
  • !exportname(nsym->name): これは、メソッド名(nsym->name)が非エクスポート(つまり、小文字で始まる)であるかどうかをチェックします。exportname関数は、Goの識別子がエクスポートされているかどうかを判断するユーティリティ関数です。

この2つの条件が真である場合、つまり、異なるパッケージに属する非エクスポートメソッドである場合に、生成されるシンボル名にnsym->pkg->prefix(メソッドが定義されているパッケージのプレフィックス)が追加されるようになりました。

例:

  • 変更前: main.localT.m (mainパッケージのlocalT型のmメソッド)
  • 変更後: main.localT.main.m (mainパッケージのlocalT型のmメソッド) または lib.localT.lib.m (libパッケージのlocalT型のmメソッド)

このようにパッケージプレフィックスをシンボル名に含めることで、コンパイラは異なるパッケージに存在する同じ名前の非エクスポートメソッドを明確に区別できるようになり、シンボル名の衝突が解消されます。

テストファイルも、test/bugs/424.goからtest/fixedbugs/bug424.goにリネームされ、test/bugs/424.dirディレクトリがtest/fixedbugs/bug424.dirに移動されました。これは、バグが修正されたことを示す標準的な慣習です。また、テストコード自体も、reflectパッケージとfmtパッケージを使用して、メソッドの解決が正しく行われているかをより詳細に検証するように更新されています。特に、reflect.TypeOf(i).Method(j)を使ってメソッドのパッケージパスと名前を動的に取得し、デバッグ情報を出力する部分が追加されています。

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

変更は主にsrc/cmd/gc/dcl.cファイル内のmethodsym関数に集中しています。

--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -1195,10 +1195,17 @@ methodsym(Sym *nsym, Type *t0, int iface)
 		if(t0->width < types[tptr]->width)
 			suffix = "·i";
 	}\n-	if(t0->sym == S && isptr[t0->etype])
-\t\tp = smprint("(%-hT).%s%s", t0, nsym->name, suffix);
-\telse
-\t\tp = smprint("%-hT.%s%s", t0, nsym->name, suffix);
+\tif(nsym->pkg != s->pkg && !exportname(nsym->name)) {
+\t\tif(t0->sym == S && isptr[t0->etype])
+\t\t\tp = smprint("(%-hT).%s.%s%s", t0, nsym->pkg->prefix, nsym->name, suffix);
+\t\telse
+\t\t\tp = smprint("%-hT.%s.%s%s", t0, nsym->pkg->prefix, nsym->name, suffix);
+\t} else {
+\t\tif(t0->sym == S && isptr[t0->etype])
+\t\t\tp = smprint("(%-hT).%s%s", t0, nsym->name, suffix);
+\t\telse
+\t\t\tp = smprint("%-hT.%s%s", t0, nsym->name, suffix);
+\t}\n 	s = pkglookup(p, s->pkg);
 	free(p);
 	return s;

また、テストファイルも以下の変更が行われています。

  • test/bugs/424.go が削除され、test/fixedbugs/bug424.go にリネームされました。
  • test/bugs/424.dir/lib.gotest/fixedbugs/bug424.dir/lib.go にリネームされました。
  • test/bugs/424.dir/main.gotest/fixedbugs/bug424.go にリネームされ、内容が更新されました。

コアとなるコードの解説

methodsym関数は、Goコンパイラがメソッドの内部表現(シンボル)を生成する際に呼び出されます。この関数は、メソッドのレシーバ型(t0)とメソッド名(nsym)を受け取り、それらを基に一意の文字列を生成します。この文字列が、コンパイラ内部でメソッドを一意に識別するためのシンボル名となります。

変更の核心は、if(nsym->pkg != s->pkg && !exportname(nsym->name))という条件分岐の追加です。

  • nsym->pkg != s->pkg: これは、現在処理しているメソッドが、そのメソッドが属するパッケージとは異なるパッケージから参照されている場合に真となります。
  • !exportname(nsym->name): これは、メソッド名が小文字で始まり、非エクスポートされている場合に真となります。

この両方の条件が真である場合、つまり「異なるパッケージから参照されている非エクスポートメソッド」である場合に、生成されるシンボル名にnsym->pkg->prefix(メソッドが定義されているパッケージのプレフィックス)が追加されます。

例えば、package mainpackage libがあり、両方にfunc (t *T) m() stringという非エクスポートメソッドが存在する場合、変更前は両方のmメソッドがT.mのようなシンボル名になる可能性がありました。しかし、変更後はmain.T.main.mlib.T.lib.mのように、パッケージ名がシンボル名に組み込まれるため、コンパイラはこれらを明確に区別できるようになります。

これにより、Goコンパイラは、異なるパッケージに存在する同じ名前の非エクスポートメソッドを誤って混同することなく、正しく解決できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード (src/cmd/gc/dcl.c)
  • Go issue tracker (go.dev/issue)
  • Go code review system (go.dev/cl)