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

[インデックス 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言語はまだ開発の非常に初期段階にあり、現在の「大文字で始まる識別子はエクスポートされ、小文字で始まる識別子はパッケージローカルである」というシンプルなルールは確立されていませんでした。

このコミットの背景には、以下のような意図があったと考えられます。

  1. 明示的な可視性制御の試み: 当時のGo言語では、C言語のようなヘッダファイルによる明示的なエクスポートメカニズムや、Java/C#のようなpublic/privateキーワードが存在しませんでした。このコミットは、exportキーワードに加えてpackageキーワードを導入することで、宣言の可視性をより明示的に制御しようとする試みの一つでした。
  2. 内部実装の隠蔽: パッケージの内部で使用されるが、外部には公開したくない関数や変数、型が存在します。これらをexportせずに、しかしパッケージ内では自由に利用できるようにするためのメカニズムが必要でした。packageキーワードは、このような内部的な宣言を明確に区別し、外部からの偶発的な利用を防ぐ目的があったと考えられます。
  3. ツールチェーンの進化: Go言語のコンパイラ(gc)、リンカ(6l)、アーカイバ(ar)は、パッケージ間の依存関係を解決し、シンボル情報をやり取りするために、パッケージのメタデータを解析する必要があります。新しい可視性ルールを導入するためには、これらのツールが新しいpackageキーワードを認識し、その情報を適切に処理できるように更新する必要がありました。

最終的に、Go言語は「大文字/小文字」による可視性ルールに落ち着きましたが、このコミットは、そのシンプルで強力なルールに至るまでの設計上の試行錯誤の一端を示しています。

前提知識の解説

このコミットを理解するためには、Go言語の初期のツールチェーンと、当時のシンボル管理に関する基本的な知識が必要です。

  1. Go言語の初期のツールチェーン:

    • gc (Go Compiler): Goソースコードをコンパイルし、オブジェクトファイルを生成するコンパイラです。このコミットでは、gcpackageキーワードを解析し、シンボルテーブルにその可視性情報を記録するように変更されています。
    • 6l (Go Linker): オブジェクトファイルをリンクして実行可能ファイルを生成するリンカです。6lは、パッケージのメタデータ(pkgdata)を解析し、シンボル解決を行います。このコミットでは、6lpackageキーワードを含むメタデータを正しく処理できるように更新されています。
    • ar (Go Archiver): 複数のオブジェクトファイルをまとめてライブラリ(アーカイブファイル)を作成するツールです。Goのパッケージは、通常、arによって作成されたアーカイブファイルとして配布されます。arもまた、パッケージのメタデータを生成・解析する役割を担っており、このコミットではpackageキーワードの情報をメタデータに含めるように変更されています。
  2. シンボルと可視性:

    • シンボル (Symbol): プログラム内の変数、関数、型などの名前(識別子)と、それに対応するメモリ上のアドレスや型情報などのメタデータを結びつけるものです。コンパイラやリンカはシンボルテーブルを用いてシンボルを管理します。
    • エクスポート (Export): あるパッケージで定義されたシンボルを、他のパッケージから参照できるようにすることです。Go言語では、現在では識別子を大文字で始めることでエクスポートされます。このコミットの時点では、exportキーワードがその役割を担っていました。
    • パッケージローカル (Package-local): あるパッケージで定義されたシンボルが、そのパッケージの内部でのみ参照可能であり、他のパッケージからは参照できないようにすることです。このコミットでは、packageキーワードがこの可視性を提供しようとしていました。
  3. パッケージデータ (pkgdata): Go言語のコンパイル済みパッケージは、そのパッケージがエクスポートするシンボルに関するメタデータを含んでいます。このメタデータは、他のパッケージがそのパッケージをインポートする際に、どのようなシンボルが利用可能であるかをリンカやコンパイラに伝えるために使用されます。このコミットでは、このpkgdataのフォーマットが拡張され、packageキーワードによる宣言情報も含まれるようになります。

技術的詳細

このコミットの技術的な変更は、主にGo言語のコンパイラ(gc)、リンカ(6l)、およびアーカイバ(ar)におけるシンボル管理とパッケージデータ処理の修正に集中しています。

  1. exportフィールドの型変更 (int から char* へ):

    • src/cmd/ar/ar.cstruct Import において、シンボルのエクスポート状態を示す export フィールドの型が int から char * に変更されました。
    • これにより、単なる真偽値(エクスポートされているか否か)だけでなく、「export」または「package」といった文字列を格納できるようになり、より詳細な可視性情報を表現できるようになりました。
    • parsepkgdata 関数も、この型変更に合わせて int *exportp から char **exportp へと引数の型が変更されています。
  2. 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キーワードが検出された場合は dcladjexportsym を、新しい packageキーワードが検出された場合は packagesym を設定するようにしています。
  3. packageキーワードの導入とシンボルへのマッピング:

    • src/cmd/gc/go.y (Yacc/Bisonの文法定義ファイル) に LPACKAGE という新しいトークンが追加され、これが package キーワードに対応します。
    • LPACKAGE が検出されると、dcladj 関数ポインタに packagesym 関数が設定されます。
    • src/cmd/gc/export.cpackagesym(Sym *s) 関数が新しく追加されました。この関数は、exportsym と同様にシンボルをエクスポートリストに追加しますが、シンボルの s->export フィールドに 2 を設定します(exportsym1 を設定)。これにより、シンボルが「パッケージローカル」であることを示します。
    • exportsympackagesym の両方で、export/package mismatch のチェックが追加されました。これは、同じシンボルが異なる可視性(exportpackage)で宣言された場合にエラーを報告するためのものです。
  4. ar6lによるパッケージデータの解析と生成の変更:

    • src/cmd/6l/go.csrc/cmd/ar/ar.cparsepkgdata 関数が、export に加えて package というプレフィックスも認識するように変更されました。
    • src/cmd/ar/ar.cloadpkgdata 関数では、インポートされたシンボルに対して、既存の export 情報と新しい package 情報が競合しないかどうかのチェックが追加されました。
    • src/cmd/ar/ar.cgetpkgdef 関数は、パッケージ定義を生成する際に、シンボルの s->export の値に応じて export または package というプレフィックスを付加するように変更されました。
    • 空のパッケージ定義の場合でも、arが解析可能な最小限の__.PKGDEFを書き出すように修正されました。
  5. gcによるpackage-localシンボルのインポート制限:

    • src/cmd/gc/export.cimportconst, importvar, importtype 関数において、インポートされるシンボルが package-local (export == 2) であり、かつそれが現在のパッケージ (!mypackage(ss)) でない場合、そのインポートをスキップするロジックが追加されました。これにより、他のパッケージのpackage-localな宣言を誤ってインポートすることを防ぎます。
    • 特に型の場合、export == 0 (非公開) または export == 2 (パッケージローカル) でかつ現在のパッケージでない場合、その型のシンボルの s->lexical フィールドを LNAME に設定するように変更されました。これは、その型名が通常のプログラムからは未定義名として扱われるようにすることで、外部からの参照をさらに制限する効果があります。

これらの変更は、Go言語のコンパイラとツールチェーンが、宣言の可視性に関するより複雑なルールを処理できるようにするための基盤を構築したものです。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. 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}\
    
  2. 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キーワードが検出された場合に、dcladjpackagesym 関数を代入することを意味します。
    • 宣言処理後には、dcladj = 0; と設定をリセットしています。
  • LPACKAGE xfndcl: 関数宣言 (xfndcl) の前に LPACKAGE が付く場合のルールが追加されました。これにより、関数もパッケージローカルとして宣言できるようになります。
  • oexport ルール: これはエクスポートキーワードの値を定義する部分です。
    • LEXPORT1 を返すのに対し、新しく追加された LPACKAGE2 を返すように定義されています。この数値がシンボルの s->export フィールドに格納され、可視性の種類を区別するために使用されます。

src/cmd/gc/export.c の変更

  • exportsym(Sym *s) 関数:
    • この関数は、シンボル s をエクスポート済みとしてマークします。
    • 変更後、s->export が既に設定されている場合(つまり、シンボルが既にエクスポート済みまたはパッケージローカルとしてマークされている場合)に、その値が 1 (エクスポート) でない場合は yyerror("export/package mismatch: %S", s) というエラーを報告するようになりました。これは、同じシンボルが exportpackage の両方で宣言されるという矛盾を防ぐためのものです。
    • 問題がなければ 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言語のコンパイラは、exportpackageという2つの異なる可視性レベルを区別し、それに応じてシンボルを管理し、インポート時のアクセス制御を行うことができるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Go言語の初期の設計に関する議論(Go Mailing Listなど、当時の情報源): (具体的なURLは特定できませんでしたが、Go言語の設計に関する歴史的な議論は、メーリングリストのアーカイブやGoブログの初期の記事に散見されます。)
  • Yacc/Bisonのドキュメント: (Go言語のコンパイラがYacc/Bisonを使用しているため、文法ファイルの理解に役立ちます。)
  • リンカ、アーカイバの一般的な概念: (これらのツールの役割は、プログラミング言語のコンパイルプロセス全般に共通する概念です。)
  • Go言語の仕様書: (現在のGo言語の仕様書は、このコミット時点とは異なる可視性ルールを定義していますが、比較することで進化を理解できます。)