[インデックス 1536] ファイルの概要
このコミットは、Go言語のコンパイラ(6g
およびgc
)とランタイムにおける、特にエクスポートされていない(小文字で始まる)メソッドの可視性と識別に関する重要な改善を導入しています。主な目的は、異なるパッケージに属する型が同じ名前のエクスポートされていないメソッドを持つ場合に発生する可能性のある衝突や誤認識を防ぐことです。また、コンパイラ内部で一時的に生成されるシンボル名に付与されていた_
(アンダースコア)プレフィックスの特殊な扱いを廃止し、コードベースを簡素化しています。
コミット
commit 61590c4c44cfa428b515a155e14c5fd7d3d5f255
Author: Russ Cox <rsc@golang.org>
Date: Wed Jan 21 14:51:57 2009 -0800
disallow P.t for lowercase t and not our package P.
implement hiding lowercase methods m in
signatures by adding in a hash of the package name
to the type hash code.
remove remaining checks for internally-generated _ names:
they are all gone.
R=ken
OCL=23236
CL=23238
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/61590c4c44cfa428b515a155e14c5fd7d3d5f255
元コミット内容
このコミットは、以下の3つの主要な変更を含んでいます。
- エクスポートされていないメソッドの可視性制御の強化: 異なるパッケージが同じ名前のエクスポートされていないメソッド(小文字で始まるメソッド)を持つ場合に、それらが誤って同じものとして扱われることを防ぎます。具体的には、メソッドのシグネチャハッシュにパッケージ名を組み込むことで、この問題を解決しています。
- 内部生成された
_
プレフィックス名の特殊扱いの廃止: コンパイラ内部で一時的に生成されるシンボル名に付与されていた_
プレフィックスに対する特別なチェックや処理をすべて削除します。これは、そのような名前がもはや生成されないか、あるいは通常のシンボル名として扱われるようになったためです。 P.t
形式のアクセス制限:P.t
という形式で、P
が現在のパッケージではない場合に、小文字で始まる識別子t
へのアクセスを禁止します。これは、Goの可視性ルール(エクスポートされていない識別子は宣言されたパッケージ内でのみ可視)を厳密に適用するためです。
変更の背景
Go言語の初期段階では、コンパイラの内部実装や言語仕様の細部が進化していました。このコミットが行われた2009年当時、Goはまだ開発途上にあり、特に型システムと可視性ルールに関する厳密な定義と実装が求められていました。
このコミットの背景には、主に以下の問題意識があったと考えられます。
- エクスポートされていないメソッドの衝突問題: Goでは、識別子(変数、関数、メソッドなど)が小文字で始まる場合、それはそのパッケージ内でのみ可視な「エクスポートされていない」識別子となります。しかし、異なるパッケージが偶然にも同じ名前のエクスポートされていないメソッドを持つ場合、コンパイラがそれらを正しく区別できない可能性がありました。特に、インターフェースの型アサーションやメソッドセットの比較において、この曖昧さがバグや予期せぬ動作を引き起こす原因となり得ました。このコミットは、メソッドのシグネチャハッシュにパッケージ情報を組み込むことで、この問題を根本的に解決しようとしています。
- コンパイラ内部の命名規則の整理: Goコンパイラは、コンパイル過程で一時的な変数やシンボルを内部的に生成します。初期のGoコンパイラでは、これらの内部生成名に
_
プレフィックスを付けて区別する慣習があったようです。しかし、このような特殊な命名規則は、コードの複雑性を増し、保守性を低下させる可能性があります。このコミットは、_
プレフィックスを持つ内部名に対する特別なチェックを削除することで、コンパイラコードの簡素化とクリーンアップを目指しています。これは、コンパイラの設計が成熟し、これらの内部名がもはや特殊な扱いを必要としないようになったことを示唆しています。 - 厳密な可視性ルールの適用: Goの可視性ルールは、モジュール性とカプセル化を保証する上で非常に重要です。
P.t
のような形式で、P
が現在のパッケージではない場合に小文字の識別子t
にアクセスしようとすることは、この可視性ルールに違反します。このコミットは、このような不正なアクセスをコンパイラレベルで明示的に禁止することで、言語の整合性を保ち、開発者が意図しない依存関係を構築するのを防ぐことを目的としています。
これらの変更は、Go言語の型システムとコンパイラの堅牢性を高め、より予測可能で安全なプログラミング環境を提供するための重要なステップでした。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な概念を理解しておく必要があります。
-
Go言語の可視性ルール(Exported vs. Unexported Identifiers):
- Goでは、識別子(変数名、関数名、型名、メソッド名など)の最初の文字が大文字である場合、その識別子は「エクスポートされている(exported)」と見なされ、そのパッケージの外部からもアクセス可能です。
- 最初の文字が小文字である場合、その識別子は「エクスポートされていない(unexported)」と見なされ、その識別子が宣言されたパッケージ内でのみアクセス可能です。他のパッケージからは直接アクセスできません。
- このルールは、Goにおけるカプセル化とモジュール性の基本的なメカニズムです。
-
メソッドシグネチャとハッシュ:
- Goのメソッドは、レシーバ型、メソッド名、引数リスト、戻り値リストによって一意に識別されます。これらをまとめて「メソッドシグネチャ」と呼びます。
- コンパイラは、型やメソッドを内部的に識別するために、それらのシグネチャからハッシュ値を生成することがよくあります。このハッシュ値は、コンパイル時の比較やルックアップの効率化に利用されます。
- このコミットでは、エクスポートされていないメソッドのハッシュ生成にパッケージ名が追加されることで、異なるパッケージの同名メソッドが区別されるようになります。
-
Goコンパイラ(
6g
,gc
)とランタイム:6g
: Go言語の初期のコンパイラの一つで、主に64ビットアーキテクチャ(AMD64)向けのコードを生成しました。Goのコンパイラは、ターゲットアーキテクチャごとに異なる名前を持つことがありました(例:8g
は32ビットIntel、5g
はARMなど)。gc
: Goコンパイラのフロントエンド部分を指す一般的な名称です。ソースコードの字句解析、構文解析、型チェック、中間コード生成など、プラットフォーム非依存の処理を担当します。6g
のようなバックエンドコンパイラと連携して動作します。- ランタイム(
src/runtime
): Goプログラムの実行をサポートする低レベルのコード群です。ガベージコレクション、スケジューラ、インターフェースのディスパッチ、メモリ管理など、Goの並行処理モデルやメモリモデルを支える重要なコンポーネントが含まれます。 - このコミットでは、コンパイラ(
6g
,gc
)だけでなく、ランタイム(iface.c
)にも変更が及んでおり、インターフェースの動作にも影響があることが示唆されます。
-
シンボルテーブルとルックアップ:
- コンパイラは、プログラム内のすべての識別子(変数、関数、型など)に関する情報を「シンボルテーブル」に格納します。
- ソースコードを解析する際、コンパイラはシンボルテーブルを「ルックアップ」して、識別子の型、スコープ、定義などの情報を取得します。
- このコミットの
lex.c
における変更は、このシンボルルックアップの挙動、特にエクスポートされていない識別子の解決方法に影響を与えます。
-
_
(アンダースコア)プレフィックス:- Go言語では、
_
は特別な意味を持つことがあります(例: 空白識別子)。 - このコミットの背景では、Goコンパイラの初期実装において、内部的に生成される一時的なシンボル名に
_
プレフィックスを付けて、通常のユーザー定義名と区別する慣習があったことが示唆されています。このコミットは、その特殊な扱いを廃止するものです。
- Go言語では、
技術的詳細
このコミットの技術的詳細は、Goコンパイラの内部動作、特に型システム、シンボル管理、およびメソッドディスパッチに深く関わっています。
-
エクスポートされていないメソッドのハッシュ計算の変更 (
src/cmd/6g/obj.c
):dumpsigt
およびdumpsigi
関数は、Goの型とメソッドの内部表現を生成する際に、それらのハッシュ値を計算します。これらのハッシュ値は、型の一致判定やインターフェースメソッドのルックアップに利用されます。- 変更前は、エクスポートされていないメソッド(
!exportname(a->name)
が真の場合)であっても、そのハッシュ計算にはメソッド名と型シグネチャのみが考慮されていました。 - 変更後、エクスポートされていないメソッドに対しては、既存のハッシュに加えて、
PRIME10
という新しい素数とパッケージ名(package
変数)のハッシュが加算されるようになりました。if(!exportname(a->name)) a->hash += PRIME10*stringhash(package);
- これにより、
mypackage.MyType.myMethod()
とanotherpackage.MyType.myMethod()
のように、異なるパッケージに属するが同じ名前のエクスポートされていないメソッドは、異なるハッシュ値を持つことになります。これは、インターフェースのメソッドセットの一致判定において、異なるパッケージの同名エクスポートされていないメソッドが誤って一致すると判断されるのを防ぎます。
-
_
プレフィックスを持つ内部名の特殊扱いの廃止:- 複数のファイル(
src/cmd/6g/gen.c
,src/cmd/6g/obj.c
,src/cmd/gc/dcl.c
,src/cmd/gc/subr.c
)で、シンボル名が_
で始まるかどうかをチェックする条件が削除されています。 - 例えば、
src/cmd/6g/gen.c
のif(t->nname != N && t->nname->sym->name[0] != '_')
という条件がif(t->nname != N)
に簡素化されています。 - これは、コンパイラがもはや
_
プレフィックスを持つ内部名を特殊な方法で生成したり、それらを特別に扱う必要がなくなったことを意味します。これにより、コンパイラのコードベースがクリーンアップされ、シンボル管理ロジックが簡素化されます。
- 複数のファイル(
-
パッケージ間のエクスポートされていない識別子のアクセス制限 (
src/cmd/gc/lex.c
):lex.c
は字句解析とシンボルルックアップを担当します。- 追加された以下のコードは、シンボル
s
がエクスポートされていない(!exportname(s->name)
)かつ、そのシンボルが宣言されたパッケージ(s->opackage
)が現在のパッケージ(package
)と異なる場合、そのシンボルを.private
という特別なコンテキストでルックアップし直すことを示しています。if(!exportname(s->name) && strcmp(package, s->opackage) != 0) s = pkglookup(s->name, ".private");
- これは、Goの可視性ルールを厳密に適用するためのものです。つまり、他のパッケージのエクスポートされていない識別子にはアクセスできないように、コンパイラが明示的にそのシンボルを「プライベート」としてマークするか、アクセスを拒否するような処理フローに誘導します。
-
型比較ロジックの簡素化 (
src/cmd/gc/subr.c
):eqtype
関数は、2つの型が等しいかどうかを比較します。以前は、シンボル名が_
で始まる場合に特別な比較ロジック(strcmp
の代わりに_
プレフィックスの有無をチェック)がありました。- このコミットでは、その特殊なロジックが削除され、
strcmp(t1->nname->sym->name, t2->nname->sym->name) != 0
という通常の文字列比較のみが行われるようになりました。これは、_
プレフィックスを持つ名前がもはや特殊な意味を持たないためです。
-
新しい素数定数
PRIME10
の追加 (src/cmd/gc/go.h
):- ハッシュ計算に使用される新しい素数
PRIME10
が定義されました。これは、パッケージ名をハッシュに組み込むために使用されます。
- ハッシュ計算に使用される新しい素数
-
ランタイムでのデバッグ出力追加 (
src/runtime/iface.c
):- インターフェース変換エラーが発生した場合に、
iface_debug
フラグが有効であれば、より詳細なデバッグ情報(インターフェースのシグネチャと型のシグネチャ)を出力するコードが追加されました。これは、上記のハッシュ変更がインターフェースの動作に与える影響をデバッグするために導入された可能性があります。
- インターフェース変換エラーが発生した場合に、
これらの変更は、Go言語のコンパイラとランタイムが、エクスポートされていない識別子のセマンティクスをより正確に処理し、内部的な命名規則を簡素化するための重要なステップでした。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下の3つのファイルに集中しています。
-
src/cmd/6g/obj.c
: エクスポートされていないメソッドのハッシュ計算にパッケージ名を追加する部分。--- a/src/cmd/6g/obj.c +++ b/src/cmd/6g/obj.c @@ -658,6 +658,8 @@ dumpsigt(Type *progt, Type *ifacet, Type *rcvrt, Type *methodt, Sym *s) a->name = method->name; a->hash = PRIME8*stringhash(a->name) + PRIME9*typehash(f->type, 0); + if(!exportname(a->name)) + a->hash += PRIME10*stringhash(package); a->perm = o; a->sym = methodsym(method, rcvrt); @@ -784,19 +785,15 @@ dumpsigi(Type *t, Sym *s) s1 = f->sym; if(s1 == nil) continue; - if(s1->name[0] == '_') - continue; b = mal(sizeof(*b)); b->link = a; a = b; a->name = s1->name; - sp = strchr(s1->name, '_'); - if(sp != nil) - a->name = sp+1; - a->hash = PRIME8*stringhash(a->name) + PRIME9*typehash(f->type, 0); + if(!exportname(a->name)) + a->hash += PRIME10*stringhash(package); a->perm = o; a->sym = methodsym(f->sym, t); a->offset = 0;
-
src/cmd/gc/lex.c
: 他のパッケージのエクスポートされていない識別子へのアクセスを制限する部分。--- a/src/cmd/gc/lex.c +++ b/src/cmd/gc/lex.c @@ -685,6 +685,8 @@ talph: ts = pkglookup(s->name, context); if(s->lexical == LIGNORE) goto l0; + if(!exportname(s->name) && strcmp(package, s->opackage) != 0) + s = pkglookup(s->name, ".private"); } DBG("lex: %S %s\n", s, lexname(s->lexical));
-
src/cmd/gc/go.h
: 新しい素数定数PRIME10
の定義。--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -34,6 +34,7 @@ enum PRIME7 = 10067, PRIME8 = 10079, PRIME9 = 10091, + PRIME10 = 10093, AUNK = 100, // these values are known by runtime
コアとなるコードの解説
上記のコアとなるコード変更箇所について、それぞれ詳しく解説します。
-
src/cmd/6g/obj.c
の変更:dumpsigt
とdumpsigi
は、それぞれ型(Type
)とインターフェース(Iface
)のシグネチャをダンプ(内部表現を生成)する関数です。これらの関数内で、メソッドのハッシュ値が計算されます。a->hash = PRIME8*stringhash(a->name) + PRIME9*typehash(f->type, 0);
の行は、メソッド名(a->name
)とメソッドの型シグネチャ(f->type
)に基づいてハッシュ値を計算しています。PRIME8
とPRIME9
は、ハッシュ衝突を減らすための素数です。- 追加された
if(!exportname(a->name)) a->hash += PRIME10*stringhash(package);
の行がこのコミットの核心です。exportname(a->name)
は、メソッド名a->name
がエクスポートされている(大文字で始まる)かどうかを判定する関数です。!exportname(a->name)
は、メソッドがエクスポートされていない(小文字で始まる)場合に真となります。- この条件が真の場合、つまりエクスポートされていないメソッドの場合にのみ、
PRIME10
と現在のパッケージ名(package
変数)のハッシュ値が既存のメソッドハッシュに加算されます。 - これにより、
packageA.Type.methodA
とpackageB.Type.methodA
のように、異なるパッケージに属する同名のエクスポートされていないメソッドは、パッケージ名のハッシュが異なるため、最終的なメソッドハッシュ値も異なることになります。これは、Goのインターフェースがメソッドセットの一致を判定する際に、異なるパッケージの同名エクスポートされていないメソッドを誤って同じものとして扱わないようにするために不可欠です。
- また、
_
プレフィックスを持つ内部名に関するチェック(if(s1->name[0] == '_') continue;
やsp = strchr(s1->name, '_'); if(sp != nil) a->name = sp+1;
)が削除されています。これは、コンパイラがもはやそのような内部名を生成しないか、あるいはそれらを通常のシンボル名として扱うようになったため、コードが簡素化されたことを示しています。
-
src/cmd/gc/lex.c
の変更:lex.c
は、Goソースコードの字句解析(トークン化)とシンボルルックアップを担当するファイルです。- 追加された
if(!exportname(s->name) && strcmp(package, s->opackage) != 0) s = pkglookup(s->name, ".private");
の行は、Goの可視性ルールを厳密に適用するためのものです。!exportname(s->name)
: シンボルs
がエクスポートされていない(小文字で始まる)ことを意味します。strcmp(package, s->opackage) != 0
: シンボルs
が宣言されたパッケージ(s->opackage
)が、現在コンパイル中のパッケージ(package
)と異なることを意味します。- この両方の条件が真の場合、つまり、他のパッケージで宣言されたエクスポートされていないシンボルにアクセスしようとしている場合、
s = pkglookup(s->name, ".private");
が実行されます。これは、そのシンボルを.private
という特別なコンテキストで再ルックアップすることを意味します。この.private
コンテキストは、通常、アクセスが許可されないことを示すか、コンパイルエラーを発生させるための内部的なメカニズムであると考えられます。これにより、Goの可視性ルールがコンパイラレベルで強制され、他のパッケージのエクスポートされていない識別子への不正なアクセスが防止されます。
-
src/cmd/gc/go.h
の変更:- このファイルは、Goコンパイラ全体で共有される定数やデータ構造の定義を含んでいます。
PRIME10 = 10093,
の追加は、上記のsrc/cmd/6g/obj.c
におけるメソッドハッシュ計算の変更で使用される新しい素数定数を定義しています。ハッシュ関数に新しい要素(パッケージ名)を組み込む際に、既存の素数とは異なる新しい素数を使用することで、ハッシュ衝突の可能性をさらに低減し、ハッシュ関数の品質を維持します。
これらの変更は、Go言語のセマンティクス、特に可視性ルールとインターフェースの動作をより堅牢で予測可能なものにするための、コンパイラ内部の重要な調整を示しています。
関連リンク
- Go言語の仕様: https://go.dev/ref/spec (特に "Exported identifiers" のセクション)
- Go言語の歴史に関する情報: https://go.dev/doc/history
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(GitHubリポジトリ)
- Go言語のコンパイラに関する一般的な知識
- ハッシュ関数と素数に関する一般的な情報