[インデックス 14047] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における組み込み型(builtin types)の埋め込みフィールドの扱いに関するバグ修正です。具体的には、埋め込まれた組み込み型が誤ってエクスポート(公開)されてしまう問題を解決し、Go言語の可視性ルール(エクスポートされないフィールドへのアクセス制限)が正しく適用されるようにします。また、reflect
パッケージがこれらのフィールドを誤ってエクスポート済みと判断しないように修正します。
コミット
cmd/gc
: 埋め込まれた組み込み型をエクスポートしないように修正。
このコミットは、Goコンパイラが構造体に埋め込まれた組み込み型(例: int
, string
など)のフィールドを誤ってエクスポート済みとして扱ってしまうバグを修正します。これにより、外部パッケージから本来アクセスできないはずのフィールドにアクセスできてしまう問題や、reflect
パッケージがフィールドの可視性を誤って報告する問題が解決されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a45777fe9975583cac3ef7ee5d61937f5a003c5a
元コミット内容
commit a45777fe9975583cac3ef7ee5d61937f5a003c5a
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Sun Oct 7 06:53:57 2012 +0200
cmd/gc: Don't export embedded builtins
Fixes #4124.
R=golang-dev, dave, minux.ma, remyoudompheng, rsc
CC=golang-dev
https://golang.org/cl/6543057
変更の背景
Go言語には、構造体への型の埋め込み(embedding)という機能があります。これにより、ある構造体が別の構造体やインターフェースのメソッドやフィールドを「昇格」させ、あたかも自身のメンバーであるかのように振る舞わせることができます。しかし、この機能が組み込み型(int
, string
など)に対して使用された場合、Goコンパイラ(cmd/gc
)がその埋め込まれた組み込み型のフィールドを誤って「エクスポート済み」(つまり、パッケージ外部からアクセス可能)として扱ってしまうバグが存在しました。
このバグは、以下の2つの主要な問題を引き起こしていました。
- コンパイル時の可視性違反: 外部パッケージから、本来アクセスできないはずの埋め込まれた組み込み型のフィールドにアクセスしようとすると、コンパイラがエラーを報告せずにコンパイルを許可してしまう可能性がありました。Goの可視性ルールでは、小文字で始まるフィールドは非公開(unexported)であり、パッケージ内からのみアクセス可能です。
- リフレクションの誤った情報:
reflect
パッケージは、Goプログラムの実行時に型情報を検査・操作するための機能を提供します。このバグにより、reflect
パッケージが埋め込まれた組み込み型のフィールドを誤って「エクスポート済み」と報告し、CanSet()
などの操作を許可してしまう可能性がありました。これにより、本来変更できないはずの非公開フィールドがリフレクションを通じて変更されてしまうという、予期せぬ動作につながる恐れがありました。
このコミットは、これらの問題を修正し、Go言語の可視性ルールとリフレクションの動作が、埋め込まれた組み込み型に対しても一貫して適用されるようにするために行われました。
前提知識の解説
1. Go言語の型の埋め込み (Embedding)
Go言語では、構造体の中にフィールド名なしで別の型を宣言することで、その型のフィールドやメソッドを現在の構造体に「埋め込む」ことができます。これにより、埋め込まれた型のフィールドやメソッドが、あたかも現在の構造体の直接のメンバーであるかのようにアクセスできるようになります。これは継承とは異なり、コンポジション(合成)の一種と見なされます。
例:
type Inner struct {
ID int
name string // 非公開フィールド
}
type Outer struct {
Inner // Inner型を埋め込み
Description string
}
func main() {
o := Outer{Inner: Inner{ID: 1, name: "test"}, Description: "example"}
fmt.Println(o.ID) // Inner.ID にアクセス
// fmt.Println(o.name) // エラー: o.name は非公開フィールドのためアクセス不可
}
2. Go言語の可視性ルール (Exported/Unexported Identifiers)
Go言語では、識別子(変数名、関数名、型名、フィールド名など)の最初の文字が大文字か小文字かによって、その識別子の可視性(スコープ)が決定されます。
- 大文字で始まる識別子: エクスポート済み(Exported)。パッケージ外部からアクセス可能です。
- 小文字で始まる識別子: 非エクスポート済み(Unexported)。宣言されたパッケージ内からのみアクセス可能です。
このルールは、カプセル化を強制し、APIの安定性を保つために重要です。
3. reflect
パッケージ
reflect
パッケージは、Goプログラムの実行時に、変数や型の情報を動的に検査・操作するための機能を提供します。これにより、プログラムは自身の構造を調べたり、実行時に未知の型の値を操作したりすることができます。
reflect.Value
型は、Goの値をリフレクションで表現したものです。reflect.Value
のメソッドには、フィールドがエクスポートされているかどうかを判断するCanSet()
などがあります。CanSet()
は、そのreflect.Value
が変更可能(settable)であるかどうかを返します。変更可能であるためには、その値がアドレス可能(addressable)であり、かつエクスポートされたフィールドである必要があります。非エクスポートのフィールドは、たとえアドレス可能であってもCanSet()
はfalse
を返します。
4. Goコンパイラ (cmd/gc
)
cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。このコンパイラは、Go言語の仕様に厳密に従ってコードを解析し、型チェック、最適化、コード生成などを行います。本コミットは、このコンパイラの内部的な型処理ロジック、特にシンボル(識別子)の管理と型情報の生成部分に修正を加えています。
技術的詳細
このコミットの技術的な核心は、Goコンパイラが埋め込まれた組み込み型(例: int
)のフィールドを処理する際に、そのフィールドの「エクスポート状態」を正しく判断するように変更することです。
Goコンパイラ内部では、型やシンボル(識別子)は特定のデータ構造で表現されます。このコミット以前は、埋め込まれた組み込み型の場合、そのシンボルがbuiltinpkg
(組み込みパッケージ)に属しているかどうかをチェックするロジックが不適切でした。これにより、本来非公開であるべきフィールドが、コンパイラ内部でエクスポート済みとして扱われてしまい、結果として外部からのアクセスやリフレクションによる操作が許可されてしまう問題が発生していました。
修正のポイントは以下の通りです。
-
dcl.c
におけるembedded
関数の修正:embedded
関数は、埋め込まれたフィールドのシンボルを処理する際に、そのフィールドがエクスポートされるべきかどうかを判断します。以前はexportname(name) || s->pkg == builtinpkg
という条件でエクスポートを判断していました。これは、「名前がエクスポート可能である」または「シンボルが組み込みパッケージに属している」場合にエクスポート済みと見なす、というロジックでした。 しかし、組み込み型(int
など)は、たとえ埋め込まれたとしても、そのフィールド名自体は小文字で始まるため、Goの可視性ルール上は非公開であるべきです。s->pkg == builtinpkg
という条件が、この非公開性を無視してエクスポート済みと判断させてしまう原因でした。 修正後は、この条件から|| s->pkg == builtinpkg
が削除され、exportname(name)
のみでエクスポートを判断するようになりました。これにより、埋め込まれた組み込み型のフィールドは、その名前が小文字で始まる限り、正しく非公開として扱われるようになります。 -
reflect.c
における型情報の生成修正:reflect.c
は、Goの型情報をリフレクションのために生成する部分です。特に、構造体のフィールドに関する情報を生成する際に、そのフィールドが組み込み型である場合に、そのパッケージパスをbuiltinpkg
ではなくlocalpkg
(現在のパッケージ)として扱うように変更されました。 以前は、埋め込まれた組み込み型の場合でも、その型がbuiltinpkg
に属していると判断し、その結果、リフレクション情報が誤ってエクスポート済みとして生成される可能性がありました。 修正では、t1->type->sym->pkg == builtinpkg
という条件が追加され、もし型が組み込みパッケージに属している場合でも、そのフィールドが現在のパッケージに属しているかのようにdgopkgpath
関数にlocalpkg
を渡すように変更されました。これにより、リフレクションがフィールドの可視性を正しく認識し、CanSet()
などが期待通りにfalse
を返すようになります。 -
align.c
とlex.c
におけるシンボル割り当ての修正: これらのファイルでは、型の初期化や字句解析の過程で、型にシンボルを割り当てる処理が行われます。以前は、t->sym = s;
のように直接シンボルs
を割り当てていましたが、修正後はt->sym = s1;
のように、pkglookup
で取得したシンボルs1
を割り当てるように変更されています。 これは、組み込み型の場合に、その型が属するパッケージ(builtinpkg
)のシンボルを正しく参照するようにするための変更です。これにより、コンパイラ内部での型とシンボルの関連付けがより正確になり、前述の可視性判断のロジックと整合性が取れるようになります。 -
fmt.c
におけるデバッグ出力の修正:fmt.c
は、コンパイラのデバッグ出力やエラーメッセージのフォーマットを担当するファイルです。この変更は、デバッグ出力のフォーマットを%T:%N
から%hhS:%N
に変更し、型のシンボルをより正確に表示するように修正しています。これは直接的なバグ修正というよりは、デバッグ情報の改善に関連する変更です。
これらの変更により、Goコンパイラは埋め込まれた組み込み型のフィールドを、Go言語の可視性ルールに従って正しく非公開として扱い、リフレクションもその事実を正確に反映するようになりました。
コアとなるコードの変更箇所
src/cmd/gc/align.c
--- a/src/cmd/gc/align.c
+++ b/src/cmd/gc/align.c
@@ -607,7 +607,7 @@ typeinit(void)
fatal("typeinit: %s already defined", s->name);
tt = typ(etype);
- tt->sym = s;
+ tt->sym = s1;
dowidth(t);
types[etype] = t;
src/cmd/gc/dcl.c
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -982,7 +982,7 @@ embedded(Sym *s)
*utfrune(name, CenterDot) = 0;
}
- if(exportname(name) || s->pkg == builtinpkg) // old behaviour, tests pass, but is it correct?
+ if(exportname(name))
n = newname(lookup(name));
else
n = newname(pkglookup(name, s->pkg));
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1181,7 +1181,7 @@ exprfmt(Fmt *f, Node *n, int prec)
tt = l->n->left->type->type;
if(t->sym == S)
tt = t->type;
- fmtprint(f, " %T:%N", t, l->n->right);
+ fmtprint(f, " %hhS:%N", t->sym, l->n->right);
} else
fmtprint(f, " %hhS:%N", l->n->left->sym, l->n->right);
src/cmd/gc/lex.c
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -1831,16 +1831,16 @@ lexinit(void)
if(etype != Txxx) {
if(etype < 0 || etype >= nelem(types))
fatal("lexinit: %s bad etype", s->name);
+ s1 = pkglookup(syms[i].name, builtinpkg);
tt = types[etype];
if(t == T) {
tt = typ(etype);
- tt->sym = s;
+ tt->sym = s1;
if(etype != TANY && etype != TSTRING)
dowidth(t);
types[etype] = t;
}
- s1 = pkglookup(syms[i].name, builtinpkg);
s1->lexical = LNAME;
s1->def = typenod(t);
continue;
src/cmd/gc/reflect.c
--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -866,7 +866,10 @@ ok:
ot = dgopkgpath(s, ot, t1->sym->pkg);
} else {
ot = dgostringptr(s, ot, nil);
- ot = dgostringptr(s, ot, nil);
+ if(t1->type->sym != S && t1->type->sym->pkg == builtinpkg)
+ ot = dgopkgpath(s, ot, localpkg);
+ else
+ ot = dgostringptr(s, ot, nil);
}
ot = dsymptr(s, ot, dtypesym(t1->type), 0);
ot = dgostrlitptr(s, ot, t1->note);
テストファイル (test/fixedbugs/
)
test/fixedbugs/bug460.dir/a.go
(新規追加)test/fixedbugs/bug460.dir/b.go
(新規追加)test/fixedbugs/bug460.go
(新規追加)test/fixedbugs/bug461.go
(新規追加)
コアとなるコードの解説
src/cmd/gc/dcl.c
の変更 (embedded
関数)
- if(exportname(name) || s->pkg == builtinpkg)
+ if(exportname(name))
この変更が、このコミットの最も重要な修正点の一つです。embedded
関数は、構造体に埋め込まれたフィールドのシンボルを処理する際に、そのフィールドがエクスポートされるべきかどうかを判断します。
以前のコードでは、フィールドの名前がエクスポート可能である(exportname(name)
)か、またはそのシンボルがbuiltinpkg
(組み込みパッケージ、例: int
, string
などが属する)に属している場合に、そのフィールドをエクスポート済みと見なしていました。
しかし、Goの可視性ルールでは、フィールド名が小文字で始まる場合(例: int
型を埋め込んだ際のフィールド名int
)は、たとえそれが組み込み型であっても非公開であるべきです。s->pkg == builtinpkg
という条件が、この非公開性を無視してエクスポート済みと判断させてしまう原因でした。
この条件を削除することで、埋め込まれた組み込み型のフィールドは、その名前が小文字で始まる限り、Goの可視性ルールに従って正しく非公開として扱われるようになります。これにより、外部パッケージからの不正なアクセスがコンパイル時に検出されるようになります。
src/cmd/gc/reflect.c
の変更
- ot = dgostringptr(s, ot, nil);
+ if(t1->type->sym != S && t1->type->sym->pkg == builtinpkg)
+ ot = dgopkgpath(s, ot, localpkg);
+ else
+ ot = dgostringptr(s, ot, nil);
この変更は、reflect
パッケージが型情報を生成する際のロジックを修正します。特に、構造体のフィールドに関する情報を生成する際に、そのフィールドが組み込み型である場合のパッケージパスの扱いを修正しています。
以前は、埋め込まれた組み込み型の場合でも、その型がbuiltinpkg
に属していると判断し、その結果、リフレクション情報が誤ってエクスポート済みとして生成される可能性がありました。
修正では、t1->type->sym != S && t1->type->sym->pkg == builtinpkg
という条件が追加されました。これは、「型が有効なシンボルを持ち、かつそのシンボルが組み込みパッケージに属している」場合に、そのフィールドのパッケージパスをlocalpkg
(現在のパッケージ)として扱うように指示します。
これにより、reflect
パッケージは埋め込まれた組み込み型のフィールドを、現在のパッケージの非公開フィールドとして正しく認識し、CanSet()
などのメソッドが期待通りにfalse
を返すようになります。これは、test/fixedbugs/bug461.go
でテストされている動作です。
src/cmd/gc/align.c
と src/cmd/gc/lex.c
の変更
- tt->sym = s;
+ tt->sym = s1;
これらの変更は、型の初期化(align.c
のtypeinit
関数)と字句解析(lex.c
のlexinit
関数)の過程で、型にシンボルを割り当てる処理を修正しています。
以前は、直接s
というシンボルを割り当てていましたが、修正後はs1
というシンボルを割り当てるように変更されています。s1
は、pkglookup(syms[i].name, builtinpkg)
によって取得されたシンボルです。
この変更は、組み込み型の場合に、その型が属するパッケージ(builtinpkg
)のシンボルを正しく参照するようにするためのものです。これにより、コンパイラ内部での型とシンボルの関連付けがより正確になり、前述のdcl.c
やreflect.c
での可視性判断ロジックと整合性が取れるようになります。これは、コンパイラが型の情報を内部的にどのように管理しているかに関わる、より低レベルな修正です。
src/cmd/gc/fmt.c
の変更
- fmtprint(f, " %T:%N", t, l->n->right);
+ fmtprint(f, " %hhS:%N", t->sym, l->n->right);
この変更は、コンパイラのデバッグ出力のフォーマットを修正しています。%T
は型全体を表示しますが、%hhS
は型のシンボルのみを表示します。
これは直接的なバグ修正というよりは、コンパイラの内部的なデバッグ情報の表示を改善するための変更であり、問題の診断や理解を助ける目的があります。
テストファイルの追加
-
test/fixedbugs/bug460.dir/a.go
-
test/fixedbugs/bug460.dir/b.go
-
test/fixedbugs/bug460.go
これらのファイルは、埋め込まれた組み込み型(int
)の非公開フィールドに外部パッケージからアクセスしようとした場合に、コンパイラが正しくエラーを報告することを確認するためのテストです。bug460.go
のコメントにある// errorcheckdir
は、このディレクトリ内のファイルがコンパイルエラーを生成することを期待するテストであることを示しています。 -
test/fixedbugs/bug461.go
このファイルは、reflect
パッケージが埋め込まれた組み込み型(int
)のフィールドを正しく非公開として認識し、CanSet()
がfalse
を返すことを確認するためのテストです。// run
は、このテストが実行され、パニックが発生しないことを期待するテストであることを示しています。
これらのテストの追加は、修正が正しく機能していることを検証し、将来的な回帰を防ぐために不可欠です。
関連リンク
- Go言語の埋め込みに関する公式ドキュメント(Go言語のバージョンによって内容が異なる可能性がありますが、概念は共通です)
- Go言語の可視性ルールに関する公式ドキュメント
参考にした情報源リンク
- コミットハッシュ:
a45777fe9975583cac3ef7ee5d61937f5a003c5a
- GitHub上のコミットページ: https://github.com/golang/go/commit/a45777fe9975583cac3ef7ee5d61937f5a003c5a
- Go言語のソースコード(特に
src/cmd/gc
とsrc/reflect
ディレクトリ) - Go言語のテストスイート(特に
test/fixedbugs
ディレクトリ) - Go言語の仕様書(識別子の可視性、型の埋め込みに関するセクション)
- Go言語のIssueトラッカー(Issue #4124に関連する情報があれば)
- Go言語のコンパイラに関する一般的な知識と資料# [インデックス 14047] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における組み込み型(builtin types)の埋め込みフィールドの扱いに関するバグ修正です。具体的には、埋め込まれた組み込み型が誤ってエクスポート(公開)されてしまう問題を解決し、Go言語の可視性ルール(エクスポートされないフィールドへのアクセス制限)が正しく適用されるようにします。また、reflect
パッケージがこれらのフィールドを誤ってエクスポート済みと判断しないように修正します。
コミット
cmd/gc
: 埋め込まれた組み込み型をエクスポートしないように修正。
このコミットは、Goコンパイラが構造体に埋め込まれた組み込み型(例: int
, string
など)のフィールドを誤ってエクスポート済みとして扱ってしまうバグを修正します。これにより、外部パッケージから本来アクセスできないはずのフィールドにアクセスできてしまう問題や、reflect
パッケージがフィールドの可視性を誤って報告する問題が解決されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a45777fe9975583cac3ef7ee5d61937f5a003c5a
元コミット内容
commit a45777fe9975583cac3ef7ee5d61937f5a003c5a
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Sun Oct 7 06:53:57 2012 +0200
cmd/gc: Don't export embedded builtins
Fixes #4124.
R=golang-dev, dave, minux.ma, remyoudompheng, rsc
CC=golang-dev
https://golang.org/cl/6543057
変更の背景
Go言語には、構造体への型の埋め込み(embedding)という機能があります。これにより、ある構造体が別の構造体やインターフェースのメソッドやフィールドを「昇格」させ、あたかも自身のメンバーであるかのように振る舞わせることができます。しかし、この機能が組み込み型(int
, string
など)に対して使用された場合、Goコンパイラ(cmd/gc
)がその埋め込まれた組み込み型のフィールドを誤って「エクスポート済み」(つまり、パッケージ外部からアクセス可能)として扱ってしまうバグが存在しました。
このバグは、以下の2つの主要な問題を引き起こしていました。
- コンパイル時の可視性違反: 外部パッケージから、本来アクセスできないはずの埋め込まれた組み込み型のフィールドにアクセスしようとすると、コンパイラがエラーを報告せずにコンパイルを許可してしまう可能性がありました。Goの可視性ルールでは、小文字で始まるフィールドは非公開(unexported)であり、パッケージ内からのみアクセス可能です。
- リフレクションの誤った情報:
reflect
パッケージは、Goプログラムの実行時に型情報を検査・操作するための機能を提供します。このバグにより、reflect
パッケージが埋め込まれた組み込み型のフィールドを誤って「エクスポート済み」と報告し、CanSet()
などの操作を許可してしまう可能性がありました。これにより、本来変更できないはずの非公開フィールドがリフレクションを通じて変更されてしまうという、予期せぬ動作につながる恐れがありました。
このコミットは、これらの問題を修正し、Go言語の可視性ルールとリフレクションの動作が、埋め込まれた組み込み型に対しても一貫して適用されるようにするために行われました。
前提知識の解説
1. Go言語の型の埋め込み (Embedding)
Go言語では、構造体の中にフィールド名なしで別の型を宣言することで、その型のフィールドやメソッドを現在の構造体に「埋め込む」ことができます。これにより、埋め込まれた型のフィールドやメソッドが、あたかも現在の構造体の直接のメンバーであるかのようにアクセスできるようになります。これは継承とは異なり、コンポジション(合成)の一種と見なされます。
例:
type Inner struct {
ID int
name string // 非公開フィールド
}
type Outer struct {
Inner // Inner型を埋め込み
Description string
}
func main() {
o := Outer{Inner: Inner{ID: 1, name: "test"}, Description: "example"}
fmt.Println(o.ID) // Inner.ID にアクセス
// fmt.Println(o.name) // エラー: o.name は非公開フィールドのためアクセス不可
}
2. Go言語の可視性ルール (Exported/Unexported Identifiers)
Go言語では、識別子(変数名、関数名、型名、フィールド名など)の最初の文字が大文字か小文字かによって、その識別子の可視性(スコープ)が決定されます。
- 大文字で始まる識別子: エクスポート済み(Exported)。パッケージ外部からアクセス可能です。
- 小文字で始まる識別子: 非エクスポート済み(Unexported)。宣言されたパッケージ内からのみアクセス可能です。
このルールは、カプセル化を強制し、APIの安定性を保つために重要です。
3. reflect
パッケージ
reflect
パッケージは、Goプログラムの実行時に、変数や型の情報を動的に検査・操作するための機能を提供します。これにより、プログラムは自身の構造を調べたり、実行時に未知の型の値を操作したりすることができます。
reflect.Value
型は、Goの値をリフレクションで表現したものです。reflect.Value
のメソッドには、フィールドがエクスポートされているかどうかを判断するCanSet()
などがあります。CanSet()
は、そのreflect.Value
が変更可能(settable)であるかどうかを返します。変更可能であるためには、その値がアドレス可能(addressable)であり、かつエクスポートされたフィールドである必要があります。非エクスポートのフィールドは、たとえアドレス可能であってもCanSet()
はfalse
を返します。
4. Goコンパイラ (cmd/gc
)
cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。このコンパイラは、Go言語の仕様に厳密に従ってコードを解析し、型チェック、最適化、コード生成などを行います。本コミットは、このコンパイラの内部的な型処理ロジック、特にシンボル(識別子)の管理と型情報の生成部分に修正を加えています。
技術的詳細
このコミットの技術的な核心は、Goコンパイラが埋め込まれた組み込み型(例: int
)のフィールドを処理する際に、そのフィールドの「エクスポート状態」を正しく判断するように変更することです。
Goコンパイラ内部では、型やシンボル(識別子)は特定のデータ構造で表現されます。このコミット以前は、埋め込まれた組み込み型の場合、そのシンボルがbuiltinpkg
(組み込みパッケージ)に属しているかどうかをチェックするロジックが不適切でした。これにより、本来非公開であるべきフィールドが、コンパイラ内部でエクスポート済みとして扱われてしまい、結果として外部からのアクセスやリフレクションによる操作が許可されてしまう問題が発生していました。
修正のポイントは以下の通りです。
-
dcl.c
におけるembedded
関数の修正:embedded
関数は、埋め込まれたフィールドのシンボルを処理する際に、そのフィールドがエクスポートされるべきかどうかを判断します。以前はexportname(name) || s->pkg == builtinpkg
という条件でエクスポートを判断していました。これは、「名前がエクスポート可能である」または「シンボルが組み込みパッケージに属している」場合にエクスポート済みと見なす、というロジックでした。 しかし、組み込み型(int
など)は、たとえ埋め込まれたとしても、そのフィールド名自体は小文字で始まるため、Goの可視性ルール上は非公開であるべきです。s->pkg == builtinpkg
という条件が、この非公開性を無視してエクスポート済みと判断させてしまう原因でした。 修正後は、この条件から|| s->pkg == builtinpkg
が削除され、exportname(name)
のみでエクスポートを判断するようになりました。これにより、埋め込まれた組み込み型のフィールドは、その名前が小文字で始まる限り、正しく非公開として扱われるようになります。 -
reflect.c
における型情報の生成修正:reflect.c
は、Goの型情報をリフレクションのために生成する部分です。特に、構造体のフィールドに関する情報を生成する際に、そのフィールドが組み込み型である場合に、そのパッケージパスをbuiltinpkg
ではなくlocalpkg
(現在のパッケージ)として扱うように変更されました。 以前は、埋め込まれた組み込み型の場合でも、その型がbuiltinpkg
に属していると判断し、その結果、リフレクション情報が誤ってエクスポート済みとして生成される可能性がありました。 修正では、t1->type->sym->pkg == builtinpkg
という条件が追加され、もし型が組み込みパッケージに属している場合でも、そのフィールドが現在のパッケージに属しているかのようにdgopkgpath
関数にlocalpkg
を渡すように変更されました。これにより、リフレクションがフィールドの可視性を正しく認識し、CanSet()
などが期待通りにfalse
を返すようになります。 -
align.c
とlex.c
におけるシンボル割り当ての修正: これらのファイルでは、型の初期化や字句解析の過程で、型にシンボルを割り当てる処理が行われます。以前は、t->sym = s;
のように直接シンボルs
を割り当てていましたが、修正後はt->sym = s1;
のように、pkglookup
で取得したシンボルs1
を割り当てるように変更されています。 これは、組み込み型の場合に、その型が属するパッケージ(builtinpkg
)のシンボルを正しく参照するようにするための変更です。これにより、コンパイラ内部での型とシンボルの関連付けがより正確になり、前述の可視性判断のロジックと整合性が取れるようになります。 -
fmt.c
におけるデバッグ出力の修正:fmt.c
は、コンパイラのデバッグ出力やエラーメッセージのフォーマットを担当するファイルです。この変更は、デバッグ出力のフォーマットを%T:%N
から%hhS:%N
に変更し、型のシンボルをより正確に表示するように修正しています。これは直接的なバグ修正というよりは、デバッグ情報の改善に関連する変更です。
これらの変更により、Goコンパイラは埋め込まれた組み込み型のフィールドを、Go言語の可視性ルールに従って正しく非公開として扱い、リフレクションもその事実を正確に反映するようになりました。
コアとなるコードの変更箇所
src/cmd/gc/align.c
--- a/src/cmd/gc/align.c
+++ b/src/cmd/gc/align.c
@@ -607,7 +607,7 @@ typeinit(void)
fatal("typeinit: %s already defined", s->name);
tt = typ(etype);
- tt->sym = s;
+ tt->sym = s1;
dowidth(t);
types[etype] = t;
src/cmd/gc/dcl.c
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -982,7 +982,7 @@ embedded(Sym *s)
*utfrune(name, CenterDot) = 0;
}
- if(exportname(name) || s->pkg == builtinpkg) // old behaviour, tests pass, but is it correct?
+ if(exportname(name))
n = newname(lookup(name));
else
n = newname(pkglookup(name, s->pkg));
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1181,7 +1181,7 @@ exprfmt(Fmt *f, Node *n, int prec)
tt = l->n->left->type->type;
if(t->sym == S)
tt = t->type;
- fmtprint(f, " %T:%N", t, l->n->right);
+ fmtprint(f, " %hhS:%N", t->sym, l->n->right);
} else
fmtprint(f, " %hhS:%N", l->n->left->sym, l->n->right);
src/cmd/gc/lex.c
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -1831,16 +1831,16 @@ lexinit(void)
if(etype != Txxx) {
if(etype < 0 || etype >= nelem(types))
fatal("lexinit: %s bad etype", s->name);
+ s1 = pkglookup(syms[i].name, builtinpkg);
tt = types[etype];
if(t == T) {
tt = typ(etype);
- tt->sym = s;
+ tt->sym = s1;
if(etype != TANY && etype != TSTRING)
dowidth(t);
types[etype] = t;
}
- s1 = pkglookup(syms[i].name, builtinpkg);
s1->lexical = LNAME;
s1->def = typenod(t);
continue;
src/cmd/gc/reflect.c
--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -866,7 +866,10 @@ ok:
ot = dgopkgpath(s, ot, t1->sym->pkg);
} else {
ot = dgostringptr(s, ot, nil);
- ot = dgostringptr(s, ot, nil);
+ if(t1->type->sym != S && t1->type->sym->pkg == builtinpkg)
+ ot = dgopkgpath(s, ot, localpkg);
+ else
+ ot = dgostringptr(s, ot, nil);
}
ot = dsymptr(s, ot, dtypesym(t1->type), 0);
ot = dgostrlitptr(s, ot, t1->note);
テストファイル (test/fixedbugs/
)
test/fixedbugs/bug460.dir/a.go
(新規追加)test/fixedbugs/bug460.dir/b.go
(新規追加)test/fixedbugs/bug460.go
(新規追加)test/fixedbugs/bug461.go
(新規追加)
コアとなるコードの解説
src/cmd/gc/dcl.c
の変更 (embedded
関数)
- if(exportname(name) || s->pkg == builtinpkg)
+ if(exportname(name))
この変更が、このコミットの最も重要な修正点の一つです。embedded
関数は、構造体に埋め込まれたフィールドのシンボルを処理する際に、そのフィールドがエクスポートされるべきかどうかを判断します。
以前のコードでは、フィールドの名前がエクスポート可能である(exportname(name)
)か、またはそのシンボルがbuiltinpkg
(組み込みパッケージ、例: int
, string
などが属する)に属している場合に、そのフィールドをエクスポート済みと見なしていました。
しかし、Goの可視性ルールでは、フィールド名が小文字で始まる場合(例: int
型を埋め込んだ際のフィールド名int
)は、たとえそれが組み込み型であっても非公開であるべきです。s->pkg == builtinpkg
という条件が、この非公開性を無視してエクスポート済みと判断させてしまう原因でした。
この条件を削除することで、埋め込まれた組み込み型のフィールドは、その名前が小文字で始まる限り、Goの可視性ルールに従って正しく非公開として扱われるようになります。これにより、外部パッケージからの不正なアクセスがコンパイル時に検出されるようになります。
src/cmd/gc/reflect.c
の変更
- ot = dgostringptr(s, ot, nil);
+ if(t1->type->sym != S && t1->type->sym->pkg == builtinpkg)
+ ot = dgopkgpath(s, ot, localpkg);
+ else
+ ot = dgostringptr(s, ot, nil);
この変更は、reflect
パッケージが型情報を生成する際のロジックを修正します。特に、構造体のフィールドに関する情報を生成する際に、そのフィールドが組み込み型である場合のパッケージパスの扱いを修正しています。
以前は、埋め込まれた組み込み型の場合でも、その型がbuiltinpkg
に属していると判断し、その結果、リフレクション情報が誤ってエクスポート済みとして生成される可能性がありました。
修正では、t1->type->sym != S && t1->type->sym->pkg == builtinpkg
という条件が追加されました。これは、「型が有効なシンボルを持ち、かつそのシンボルが組み込みパッケージに属している」場合に、そのフィールドのパッケージパスをlocalpkg
(現在のパッケージ)として扱うように指示します。
これにより、reflect
パッケージは埋め込まれた組み込み型のフィールドを、現在のパッケージの非公開フィールドとして正しく認識し、CanSet()
などのメソッドが期待通りにfalse
を返すようになります。これは、test/fixedbugs/bug461.go
でテストされている動作です。
src/cmd/gc/align.c
と src/cmd/gc/lex.c
の変更
- tt->sym = s;
+ tt->sym = s1;
これらの変更は、型の初期化(align.c
のtypeinit
関数)と字句解析(lex.c
のlexinit
関数)の過程で、型にシンボルを割り当てる処理を修正しています。
以前は、直接s
というシンボルを割り当てていましたが、修正後はs1
というシンボルを割り当てるように変更されています。s1
は、pkglookup(syms[i].name, builtinpkg)
によって取得されたシンボルです。
この変更は、組み込み型の場合に、その型が属するパッケージ(builtinpkg
)のシンボルを正しく参照するようにするためのものです。これにより、コンパイラ内部での型とシンボルの関連付けがより正確になり、前述のdcl.c
やreflect.c
での可視性判断ロジックと整合性が取れるようになります。これは、コンパイラが型の情報を内部的にどのように管理しているかに関わる、より低レベルな修正です。
src/cmd/gc/fmt.c
の変更
- fmtprint(f, " %T:%N", t, l->n->right);
+ fmtprint(f, " %hhS:%N", t->sym, l->n->right);
この変更は、コンパイラのデバッグ出力のフォーマットを修正しています。%T
は型全体を表示しますが、%hhS
は型のシンボルのみを表示します。
これは直接的なバグ修正というよりは、コンパイラの内部的なデバッグ情報の表示を改善するための変更であり、問題の診断や理解を助ける目的があります。
テストファイルの追加
-
test/fixedbugs/bug460.dir/a.go
-
test/fixedbugs/bug460.dir/b.go
-
test/fixedbugs/bug460.go
これらのファイルは、埋め込まれた組み込み型(int
)の非公開フィールドに外部パッケージからアクセスしようとした場合に、コンパイラが正しくエラーを報告することを確認するためのテストです。bug460.go
のコメントにある// errorcheckdir
は、このディレクトリ内のファイルがコンパイルエラーを生成することを期待するテストであることを示しています。 -
test/fixedbugs/bug461.go
このファイルは、reflect
パッケージが埋め込まれた組み込み型(int
)のフィールドを正しく非公開として認識し、CanSet()
がfalse
を返すことを確認するためのテストです。// run
は、このテストが実行され、パニックが発生しないことを期待するテストであることを示しています。
これらのテストの追加は、修正が正しく機能していることを検証し、将来的な回帰を防ぐために不可欠です。
関連リンク
- Go言語の埋め込みに関する公式ドキュメント(Go言語のバージョンによって内容が異なる可能性がありますが、概念は共通です)
- Go言語の可視性ルールに関する公式ドキュメント
参考にした情報源リンク
- コミットハッシュ:
a45777fe9975583cac3ef7ee5d61937f5a003c5a
- GitHub上のコミットページ: https://github.com/golang/go/commit/a45777fe9975583cac3ef7ee5d61937f5a003c5a
- Go言語のソースコード(特に
src/cmd/gc
とsrc/reflect
ディレクトリ) - Go言語のテストスイート(特に
test/fixedbugs
ディレクトリ) - Go言語の仕様書(識別子の可視性、型の埋め込みに関するセクション)
- Go言語のIssueトラッカー(Issue #4124に関連する情報があれば)
- Go言語のコンパイラに関する一般的な知識と資料