[インデックス 1130] ファイルの概要
このコミットは、Go言語の初期段階において、パッケージ内での宣言の可視性を制御するための新しいメカニズムを導入するものです。具体的には、package
という新しいキーワードを導入し、これにより宣言がそのパッケージ内でのみ可視となる「パッケージローカル」なスコープを持つようにします。これは、既存のexport
キーワード(パッケージ外に公開される宣言)とは異なる可視性レベルを提供します。コンパイラ(gc
)、リンカ(6l
)、およびアーカイバ(ar
)がこの新しい可視性ルールを認識し、適切に処理するように変更されています。
コミット
package-local declarations using keyword "package".
R=r
DELTA=129 (81 added, 0 deleted, 48 changed)
OCL=19283
CL=19291
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d3f6093931565cebffd499696e879ec34318c519
元コミット内容
このコミットは、Go言語の宣言に「パッケージローカル」な可視性という概念を導入します。これは、package
という新しいキーワードを用いて実現されます。これにより、宣言されたエンティティ(変数、型、関数、定数など)は、その宣言が行われたパッケージの内部でのみアクセス可能となり、他のパッケージからは参照できなくなります。これは、既存のexport
キーワード(現在のGo言語における大文字で始まる識別子に相当)が提供する、パッケージ外部への公開とは異なる、より限定的なスコープを提供します。
変更の背景
Go言語の設計初期段階において、識別子の可視性制御は重要な課題でした。このコミットが行われた2008年11月時点では、Go言語はまだ開発の非常に初期段階にあり、現在の「大文字で始まる識別子はエクスポートされ、小文字で始まる識別子はパッケージローカルである」というシンプルなルールは確立されていませんでした。
このコミットの背景には、以下のような意図があったと考えられます。
- 明示的な可視性制御の試み: 当時のGo言語では、C言語のようなヘッダファイルによる明示的なエクスポートメカニズムや、Java/C#のような
public
/private
キーワードが存在しませんでした。このコミットは、export
キーワードに加えてpackage
キーワードを導入することで、宣言の可視性をより明示的に制御しようとする試みの一つでした。 - 内部実装の隠蔽: パッケージの内部で使用されるが、外部には公開したくない関数や変数、型が存在します。これらを
export
せずに、しかしパッケージ内では自由に利用できるようにするためのメカニズムが必要でした。package
キーワードは、このような内部的な宣言を明確に区別し、外部からの偶発的な利用を防ぐ目的があったと考えられます。 - ツールチェーンの進化: Go言語のコンパイラ(
gc
)、リンカ(6l
)、アーカイバ(ar
)は、パッケージ間の依存関係を解決し、シンボル情報をやり取りするために、パッケージのメタデータを解析する必要があります。新しい可視性ルールを導入するためには、これらのツールが新しいpackage
キーワードを認識し、その情報を適切に処理できるように更新する必要がありました。
最終的に、Go言語は「大文字/小文字」による可視性ルールに落ち着きましたが、このコミットは、そのシンプルで強力なルールに至るまでの設計上の試行錯誤の一端を示しています。
前提知識の解説
このコミットを理解するためには、Go言語の初期のツールチェーンと、当時のシンボル管理に関する基本的な知識が必要です。
-
Go言語の初期のツールチェーン:
gc
(Go Compiler): Goソースコードをコンパイルし、オブジェクトファイルを生成するコンパイラです。このコミットでは、gc
がpackage
キーワードを解析し、シンボルテーブルにその可視性情報を記録するように変更されています。6l
(Go Linker): オブジェクトファイルをリンクして実行可能ファイルを生成するリンカです。6l
は、パッケージのメタデータ(pkgdata
)を解析し、シンボル解決を行います。このコミットでは、6l
がpackage
キーワードを含むメタデータを正しく処理できるように更新されています。ar
(Go Archiver): 複数のオブジェクトファイルをまとめてライブラリ(アーカイブファイル)を作成するツールです。Goのパッケージは、通常、ar
によって作成されたアーカイブファイルとして配布されます。ar
もまた、パッケージのメタデータを生成・解析する役割を担っており、このコミットではpackage
キーワードの情報をメタデータに含めるように変更されています。
-
シンボルと可視性:
- シンボル (Symbol): プログラム内の変数、関数、型などの名前(識別子)と、それに対応するメモリ上のアドレスや型情報などのメタデータを結びつけるものです。コンパイラやリンカはシンボルテーブルを用いてシンボルを管理します。
- エクスポート (Export): あるパッケージで定義されたシンボルを、他のパッケージから参照できるようにすることです。Go言語では、現在では識別子を大文字で始めることでエクスポートされます。このコミットの時点では、
export
キーワードがその役割を担っていました。 - パッケージローカル (Package-local): あるパッケージで定義されたシンボルが、そのパッケージの内部でのみ参照可能であり、他のパッケージからは参照できないようにすることです。このコミットでは、
package
キーワードがこの可視性を提供しようとしていました。
-
パッケージデータ (
pkgdata
): Go言語のコンパイル済みパッケージは、そのパッケージがエクスポートするシンボルに関するメタデータを含んでいます。このメタデータは、他のパッケージがそのパッケージをインポートする際に、どのようなシンボルが利用可能であるかをリンカやコンパイラに伝えるために使用されます。このコミットでは、このpkgdata
のフォーマットが拡張され、package
キーワードによる宣言情報も含まれるようになります。
技術的詳細
このコミットの技術的な変更は、主にGo言語のコンパイラ(gc
)、リンカ(6l
)、およびアーカイバ(ar
)におけるシンボル管理とパッケージデータ処理の修正に集中しています。
-
export
フィールドの型変更 (int
からchar*
へ):src/cmd/ar/ar.c
のstruct Import
において、シンボルのエクスポート状態を示すexport
フィールドの型がint
からchar *
に変更されました。- これにより、単なる真偽値(エクスポートされているか否か)だけでなく、「
export
」または「package
」といった文字列を格納できるようになり、より詳細な可視性情報を表現できるようになりました。 parsepkgdata
関数も、この型変更に合わせてint *exportp
からchar **exportp
へと引数の型が変更されています。
-
dcladj
関数ポインタの導入:src/cmd/gc/go.h
において、グローバル変数exportadj
(int
) がdcladj
(void (*dcladj)(Sym*)
) という関数ポインタに置き換えられました。src/cmd/gc/dcl.c
では、変数、定数、型の宣言時にexportsym(n->sym)
を直接呼び出す代わりにdcladj(n->sym)
を呼び出すように変更されました。- この変更により、宣言時にシンボルに対して適用される調整(エクスポート処理やパッケージローカル処理)を動的に切り替えることが可能になりました。
go.y
(Go言語の文法定義ファイル) で、export
キーワードが検出された場合はdcladj
にexportsym
を、新しいpackage
キーワードが検出された場合はpackagesym
を設定するようにしています。
-
package
キーワードの導入とシンボルへのマッピング:src/cmd/gc/go.y
(Yacc/Bisonの文法定義ファイル) にLPACKAGE
という新しいトークンが追加され、これがpackage
キーワードに対応します。LPACKAGE
が検出されると、dcladj
関数ポインタにpackagesym
関数が設定されます。src/cmd/gc/export.c
にpackagesym(Sym *s)
関数が新しく追加されました。この関数は、exportsym
と同様にシンボルをエクスポートリストに追加しますが、シンボルのs->export
フィールドに2
を設定します(exportsym
は1
を設定)。これにより、シンボルが「パッケージローカル」であることを示します。exportsym
とpackagesym
の両方で、export/package mismatch
のチェックが追加されました。これは、同じシンボルが異なる可視性(export
とpackage
)で宣言された場合にエラーを報告するためのものです。
-
ar
と6l
によるパッケージデータの解析と生成の変更:src/cmd/6l/go.c
とsrc/cmd/ar/ar.c
のparsepkgdata
関数が、export
に加えてpackage
というプレフィックスも認識するように変更されました。src/cmd/ar/ar.c
のloadpkgdata
関数では、インポートされたシンボルに対して、既存のexport
情報と新しいpackage
情報が競合しないかどうかのチェックが追加されました。src/cmd/ar/ar.c
のgetpkgdef
関数は、パッケージ定義を生成する際に、シンボルのs->export
の値に応じてexport
またはpackage
というプレフィックスを付加するように変更されました。- 空のパッケージ定義の場合でも、
ar
が解析可能な最小限の__.PKGDEF
を書き出すように修正されました。
-
gc
によるpackage
-localシンボルのインポート制限:src/cmd/gc/export.c
のimportconst
,importvar
,importtype
関数において、インポートされるシンボルがpackage
-local (export == 2
) であり、かつそれが現在のパッケージ (!mypackage(ss)
) でない場合、そのインポートをスキップするロジックが追加されました。これにより、他のパッケージのpackage
-localな宣言を誤ってインポートすることを防ぎます。- 特に型の場合、
export == 0
(非公開) またはexport == 2
(パッケージローカル) でかつ現在のパッケージでない場合、その型のシンボルのs->lexical
フィールドをLNAME
に設定するように変更されました。これは、その型名が通常のプログラムからは未定義名として扱われるようにすることで、外部からの参照をさらに制限する効果があります。
これらの変更は、Go言語のコンパイラとツールチェーンが、宣言の可視性に関するより複雑なルールを処理できるようにするための基盤を構築したものです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/cmd/gc/go.y
: Go言語の文法定義ファイルであり、package
キーワードの導入と、それに対応する宣言処理のフックが定義されています。--- a/src/cmd/gc/go.y +++ b/src/cmd/gc/go.y @@ -193,10 +193,16 @@ xdcl: { $$ = N; } -|\tLEXPORT { exportadj = 1; stksize = initstksize; } common_dcl +|\tLEXPORT { dcladj = exportsym; stksize = initstksize; } common_dcl { $$ = $3; -\t\texportadj = 0;\n+\t\tdcladj = 0; +\t\tinitstksize = stksize; +\t} +|\tLPACKAGE { dcladj = packagesym; stksize = initstksize; } common_dcl +\t{\n+\t\t$$ = $3; +\t\tdcladj = 0; \tinitstksize = stksize; } |\tLEXPORT '(' export_list_r ')' @@ -209,6 +215,12 @@ xdcl: \t\texportsym($2->nname->sym); \t$$ = N; } +|\tLPACKAGE xfndcl +\t{\n+\t\tif($2 != N && $2->nname != N) +\t\t\tpackagesym($2->nname->sym); +\t\t$$ = N; +\t} |\t';' { $$ = N; @@ -1773,6 +1785,10 @@ oexport:\n {\n \t$$ = 1;\n }\n +|\tLPACKAGE +\t{\n+\t\t$$ = 2;\n+\t}\
-
src/cmd/gc/export.c
: シンボルのエクスポートおよびパッケージローカル化を処理する関数が定義されています。--- a/src/cmd/gc/export.c +++ b/src/cmd/gc/export.c @@ -28,13 +28,30 @@ exportsym(Sym *s)\n {\n \tif(s == S)\n \t\treturn;\n-\tif(s->export != 0)\n+\tif(s->export != 0) {\n+\t\tif(s->export != 1)\n+\t\t\tyyerror("export/package mismatch: %S", s);\n \t\treturn;\n+\t}\n \ts->export = 1;\n \n \taddexportsym(s);\n }\n \n+void\n+packagesym(Sym *s)\n+{\n+\tif(s == S)\n+\t\treturn;\n+\tif(s->export != 0) {\n+\t\tif(s->export != 2)\n+\t\t\tyyerror("export/package mismatch: %S", s);\n+\t\treturn;\n+\t}\n+\ts->export = 2;\n+\n+\taddexportsym(s);\n+}\n \n void\n dumpprereq(Type *t)\n@@ -290,12 +313,21 @@ pkgtype(char *name, char *pkg)\n \treturn s->otype;\n }\n \n+static int\n+mypackage(Node *ss)\n+{\n+\treturn strcmp(ss->psym->name, package) == 0;\n+}\n+\n void\n importconst(int export, Node *ss, Type *t, Val *v)\n {\n \tNode *n;\n \tSym *s;\n \n+\tif(export == 2 && !mypackage(ss))\n+\t\treturn;\n+\n \tn = nod(OLITERAL, N, N);\n \tn->val = *v;\n \tn->type = t;\n@@ -307,6 +339,7 @@ importconst(int export, Node *ss, Type *t, Val *v)\n \t}\n \n \tdodclconst(newname(s), n);\n+\ts->export = export;\n \n \tif(debug['e'])\n \t\tprint("import const %S\\n", s);\n@@ -317,6 +350,9 @@ importvar(int export, Node *ss, Type *t)\n {\n \tSym *s;\n \n+\tif(export == 2 && !mypackage(ss))\n+\t\treturn;\n+\n \ts = importsym(ss, LNAME);\n \tif(s->oname != N) {\n \t\tif(eqtype(t, s->oname->type, 0))\n@@ -326,6 +362,7 @@ importvar(int export, Node *ss, Type *t)\n \t}\n \tcheckwidth(t);\n \taddvar(newname(s), t, PEXTERN);\n+\ts->export = export;\n \n \tif(debug['e'])\n \t\tprint("import var %S %lT\\n", s, t);\n@@ -352,6 +389,14 @@ importtype(int export, Node *ss, Type *t)\n \ts->otype->sym = s;\n \tcheckwidth(s->otype);\n \n+\t// If type name should not be visible to importers,\n+\t// hide it by setting the lexical type to name.\n+\t// This will make references in the ordinary program\n+\t// (but not the import sections) look at s->oname,\n+\t// which is nil, as for an undefined name.\n+\tif(export == 0 || (export == 2 && !mypackage(ss)))\n+\t\ts->lexical = LNAME;\n+\n \tif(debug['e'])\n \t\tprint("import type %S %lT\\n", s, t);\n }\n ```
コアとなるコードの解説
src/cmd/gc/go.y
の変更
xdcl
ルール: これはGo言語の宣言(変数、定数、関数、型など)を処理する文法ルールです。- 以前は
LEXPORT { exportadj = 1; ... } common_dcl
のように、exportadj
という整数型のフラグを立てていました。 - 変更後、
LEXPORT { dcladj = exportsym; ... } common_dcl
となり、dcladj
という関数ポインタにexportsym
関数を代入するように変わりました。これにより、宣言処理の汎用性が向上しました。 - 新しく
LPACKAGE { dcladj = packagesym; ... } common_dcl
というルールが追加されました。これは、package
キーワードが検出された場合に、dcladj
にpackagesym
関数を代入することを意味します。 - 宣言処理後には、
dcladj = 0;
と設定をリセットしています。
- 以前は
LPACKAGE xfndcl
: 関数宣言 (xfndcl
) の前にLPACKAGE
が付く場合のルールが追加されました。これにより、関数もパッケージローカルとして宣言できるようになります。oexport
ルール: これはエクスポートキーワードの値を定義する部分です。LEXPORT
が1
を返すのに対し、新しく追加されたLPACKAGE
は2
を返すように定義されています。この数値がシンボルのs->export
フィールドに格納され、可視性の種類を区別するために使用されます。
src/cmd/gc/export.c
の変更
exportsym(Sym *s)
関数:- この関数は、シンボル
s
をエクスポート済みとしてマークします。 - 変更後、
s->export
が既に設定されている場合(つまり、シンボルが既にエクスポート済みまたはパッケージローカルとしてマークされている場合)に、その値が1
(エクスポート) でない場合はyyerror("export/package mismatch: %S", s)
というエラーを報告するようになりました。これは、同じシンボルがexport
とpackage
の両方で宣言されるという矛盾を防ぐためのものです。 - 問題がなければ
s->export = 1;
と設定します。
- この関数は、シンボル
packagesym(Sym *s)
関数 (新規追加):- この関数は
exportsym
と同様にシンボルをエクスポートリストに追加しますが、s->export
フィールドに2
を設定します。これにより、シンボルが「パッケージローカル」であることを示します。 - こちらも
exportsym
と同様に、s->export
が既に設定されている場合に、その値が2
(パッケージローカル) でない場合はyyerror("export/package mismatch: %S", s)
というエラーを報告します。
- この関数は
mypackage(Node *ss)
関数 (新規追加):- インポート処理において、インポート元のパッケージ (
ss
) が現在のコンパイル対象のパッケージと同じであるかどうかを判定するためのヘルパー関数です。
- インポート処理において、インポート元のパッケージ (
importconst
,importvar
,importtype
関数:- これらの関数は、他のパッケージから定数、変数、型をインポートする際に呼び出されます。
- 変更後、各関数の冒頭に
if(export == 2 && !mypackage(ss)) return;
というチェックが追加されました。これは、インポートしようとしているシンボルが「パッケージローカル」 (export == 2
) であり、かつそれが現在のパッケージではない (!mypackage(ss)
) 場合、そのインポート処理をスキップするという意味です。これにより、他のパッケージの内部的な宣言が誤ってインポートされることを防ぎます。 - インポートされたシンボルに対しても、その可視性情報 (
export
の値) をs->export
に設定するように変更されました。 importtype
関数では、さらに詳細な可視性制御が導入されました。if(export == 0 || (export == 2 && !mypackage(ss))) s->lexical = LNAME;
という行が追加されています。これは、型名が非公開 (export == 0
) であるか、またはパッケージローカル (export == 2
) であり、かつ現在のパッケージではない場合、そのシンボルのs->lexical
フィールドをLNAME
(未定義名) に設定するというものです。これにより、通常のプログラムからはその型名が参照できなくなり、外部からのアクセスを効果的に隠蔽します。
これらの変更により、Go言語のコンパイラは、export
とpackage
という2つの異なる可視性レベルを区別し、それに応じてシンボルを管理し、インポート時のアクセス制御を行うことができるようになりました。
関連リンク
- Go言語の初期のコミット履歴: https://github.com/golang/go/commits/master?after=d3f6093931565cebffd499696e879ec34318c519+34&path=src%2Fcmd%2Fgc%2Fgo.y (このコミット前後の履歴を辿ることで、Go言語の設計進化の過程を垣間見ることができます)
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語の初期の設計に関する議論(Go Mailing Listなど、当時の情報源): (具体的なURLは特定できませんでしたが、Go言語の設計に関する歴史的な議論は、メーリングリストのアーカイブやGoブログの初期の記事に散見されます。)
- Yacc/Bisonのドキュメント: (Go言語のコンパイラがYacc/Bisonを使用しているため、文法ファイルの理解に役立ちます。)
- リンカ、アーカイバの一般的な概念: (これらのツールの役割は、プログラミング言語のコンパイルプロセス全般に共通する概念です。)
- Go言語の仕様書: (現在のGo言語の仕様書は、このコミット時点とは異なる可視性ルールを定義していますが、比較することで進化を理解できます。)