[インデックス 1054] ファイルの概要
このコミットは、Go言語の初期段階における reflect
ライブラリのバグ修正に関するものです。具体的には、インターフェースのパース処理と、単一の戻り値を持つ関数のパース処理における不具合が修正されています。reflect
パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)することを可能にする重要な機能を提供します。
コミット
commit bdbb958895e7055e3ecd3f9c75b3d453b0ab7fff
Author: Rob Pike <r@golang.org>
Date: Wed Nov 5 08:17:01 2008 -0800
fix bugs parsing functions in reflect library.
parsed interfaces wrong.
could not handle a function with a single type as a return value.
R=rsc
DELTA=34 (20 added, 2 deleted, 12 changed)
OCL=18511
CL=18520
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bdbb958895e7055e3ecd3f9c75b3d453b0ab7fff
元コミット内容
reflect
ライブラリにおける関数パースのバグを修正。
インターフェースのパースが間違っていた。
単一の型を戻り値とする関数を扱えなかった。
レビュー担当者: rsc 変更行数: 34行 (20行追加, 2行削除, 12行変更) OCL: 18511 CL: 18520
変更の背景
Go言語の reflect
パッケージは、プログラムが実行時に型情報を動的に取得・操作するための基盤を提供します。これは、例えばJSONエンコーディング/デコーディング、データベースORM、RPCフレームワークなど、多くのメタプログラミング的なタスクにおいて不可欠な機能です。
このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の非常に初期の段階でした。この時期は、言語のコア機能や標準ライブラリが活発に開発され、多くのバグ修正や機能改善が行われていました。
コミットメッセージにある「parsed interfaces wrong」と「could not handle a function with a single type as a return value」という問題は、reflect
パッケージがGoの型システム、特にインターフェース型や関数型を正確に解析できていなかったことを示しています。これは、リフレクションを利用するGoプログラムが、これらの型を正しく扱えないという重大な問題を引き起こす可能性がありました。
具体的には、reflect.ParseTypeString
のような関数が、Goのソースコードから型定義の文字列を解析する際に、インターフェースのメソッドシグネチャや関数の戻り値のパースに失敗していたと考えられます。このようなパースの不正確さは、リフレクションベースのコードが予期せぬ挙動を示したり、クラッシュしたりする原因となります。この修正は、reflect
パッケージの堅牢性と正確性を向上させ、Go言語の安定した基盤を築く上で重要な一歩でした。
前提知識の解説
Go言語の reflect
パッケージ
Go言語の reflect
パッケージは、実行時に値の型を検査し、その値を操作するための機能を提供します。これにより、コンパイル時には型が不明なデータ(例: JSONやデータベースから読み込まれたデータ)を扱う汎用的なコードを書くことが可能になります。
reflect
パッケージの主要な概念には以下があります。
reflect.Type
: Goの型の抽象表現です。例えば、int
、string
、struct
、interface
、func
などの型情報を保持します。reflect.Value
: Goの値の抽象表現です。任意のGoの値をreflect.Value
に変換して、その値を動的に操作できます。- 型パース: 文字列形式の型定義を解析し、
reflect.Type
オブジェクトに変換するプロセスです。このコミットの主要な焦点は、この型パースの正確性に関するものです。
インターフェース型
Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。インターフェース型は、そのインターフェースで定義されたすべてのメソッドを実装する任意の具象型の値を保持できます。
例:
type Reader interface {
Read(p []byte) (n int, err error)
}
reflect
パッケージは、このようなインターフェースの構造(どのメソッドを持つか、そのメソッドのシグネチャは何か)を正確にパースできる必要があります。
関数型
Goの関数は第一級オブジェクトであり、型を持ちます。関数型は、その関数の引数と戻り値の型を定義します。
例:
type MyFunc func(int, string) (bool, error)
reflect
パッケージは、関数の引数リストと戻り値リストを正確にパースできる必要があります。特に、単一の戻り値を持つ関数(例: func() int
)や、複数の戻り値を持つ関数(例: func() (int, error)
)の区別とパースが重要です。
OCL
と CL
OCL
(Original Change List) と CL
(Change List) は、Google社内で使用されていたPerforceバージョン管理システムにおける変更セットの識別子です。Go言語は元々Google社内で開発されていたため、初期のコミットメッセージにはこれらの内部的な参照が含まれています。これらは、特定の変更がどの内部的な変更セットに対応するかを示すものであり、Goのオープンソース化に伴いGitに移行した後も、初期のコミット履歴にはその名残が残っています。
技術的詳細
このコミットは、src/lib/reflect/type.go
ファイル内の型パースロジック、特に Parser
構造体のメソッドに焦点を当てています。
Parser.Fields
メソッドの変更
Parser.Fields
メソッドは、構造体、インターフェース、および関数の引数リストのフィールド(またはパラメータ)をパースするために使用されます。元の実装では、このメソッドは sep
(セパレータ) 引数のみを受け取っていました。
変更前:
func (p *Parser) Fields(sep string) *[]Field {
// ...
for p.token != "" && !special(p.token[0]) {
// ...
}
// ...
}
この for
ループの条件 !special(p.token[0])
は、トークンが特殊文字(例: (
, )
, {
, }
など)でない限りループを続けるというものでした。しかし、インターフェースや構造体の定義において、フィールドリストの終端を示す }
のような文字を正しく認識できない場合がありました。特に、インターフェースのメソッドシグネチャ内に (
が含まれる場合、Fields()
がそこでパースを停止してしまう問題があったようです。コミットメッセージの「NOTE: INTERFACES PARSE INCORRECTLY: parser's Fields() stops at '('」というコメントがこれを裏付けています。
変更後:
func (p *Parser) Fields(sep, term string) *[]Field {
// ...
for p.token != "" && p.token != term {
// ...
}
// ...
}
Fields
メソッドに term
(ターミネータ) 引数が追加されました。これにより、フィールドのパースを終了する明確なトークン(例: }
や )
)を指定できるようになりました。これにより、パースロジックがより堅牢になり、インターフェースや関数の定義の終端を正確に認識できるようになりました。
Parser.OneField
メソッドの追加
単一の戻り値を持つ関数をパースするために、Parser.OneField
という新しいヘルパーメソッドが追加されました。
// A single type packaged as a field for a function return
func (p *Parser) OneField() *[]Field {
a := new([]Field, 1);
a[0].name = "";
a[0].typ = p.Type("");
return a;
}
このメソッドは、名前のない単一の型を Field
スライスとしてラップして返します。これは、関数の戻り値が単一の型である場合に、それをフィールドリストとして扱うための汎用的な方法を提供します。
Parser.Func
メソッドの変更
Parser.Func
メソッドは、関数型をパースする責任を負っています。このメソッドは、関数の引数リストと戻り値リストを処理します。
変更前は、戻り値が単一の型である場合を適切に処理できていませんでした。
func (p *Parser) Func(name string, tokstart int) *StubType {
// ...
f1 := NewStructTypeStruct("", "", p.Fields(",")); // 引数リスト
if p.token != ")" {
return MissingStub;
}
// ...
if p.token != "(" {
// 1 list: the in parameters only
return NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, nil));
}
p.Next();
f2 := NewStructTypeStruct("", "", p.Fields(",")); // 戻り値リスト
// ...
}
このロジックでは、引数リストの後に (
が続かない場合(つまり、戻り値リストがないか、単一の戻り値がある場合)、戻り値がないものとして扱われていました。
変更後、Parser.Func
は OneField
メソッドを活用し、単一の戻り値を持つ関数を正しくパースできるようになりました。
func (p *Parser) Func(name string, tokstart int) *StubType {
// ...
f1 := NewStructTypeStruct("", "", p.Fields(",", ")")); // 引数リストのパースにterm引数を追加
if p.token != ")" {
return MissingStub;
}
// ...
if p.token != "(" {
// 1 list: the in parameters are a list. Is there a single out parameter?
if p.token == "" || p.token == "}" || p.token == "," || p.token == ";" {
return NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, nil));
}
// A single out parameter.
f2 := NewStructTypeStruct("", "", p.OneField()); // OneField() を使用して単一の戻り値を処理
return NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, f2));
} else {
p.Next();
}
f2 := NewStructTypeStruct("", "", p.Fields(",", ")")); // 戻り値リストのパースにterm引数を追加
// ...
}
この修正により、引数リストの後に (
が続かない場合でも、現在のトークンが空文字列、}
、,
、;
でない限り、それは単一の戻り値であると判断し、OneField()
を呼び出してその戻り値をパースするようになりました。これにより、「could not handle a function with a single type as a return value」という問題が解決されました。
Parser.Struct
と Parser.Interface
メソッドの変更
Parser.Struct
と Parser.Interface
メソッドも、Fields
メソッドの呼び出しにおいて term
引数を追加するように変更されました。これにより、構造体やインターフェースの定義の終端である }
を正確に認識できるようになり、「parsed interfaces wrong」という問題の解決に寄与しています。
変更前:
func (p *Parser) Struct(name string, tokstart int) *StubType {
f := p.Fields(";");
// ...
}
func (p *Parser) Interface(name string, tokstart int) *StubType {
f := p.Fields(";");
// ...
}
変更後:
func (p *Parser) Struct(name string, tokstart int) *StubType {
f := p.Fields(";", "}");
// ...
}
func (p *Parser) Interface(name string, tokstart int) *StubType {
f := p.Fields(";", "}");
// ...
}
これにより、構造体やインターフェースのフィールド(またはメソッド)のパースが、}
トークンで確実に終了するようになりました。
コアとなるコードの変更箇所
src/lib/reflect/test.go
--- a/src/lib/reflect/test.go
+++ b/src/lib/reflect/test.go
@@ -88,7 +88,6 @@ export type empty interface {}
export type T struct { a int; b float64; c string; d *int }
func main() {
-//NOTE: INTERFACES PARSE INCORRECTLY: parser's Fields() stops at '('
var s string;
var t reflect.Type;
@@ -224,13 +223,18 @@ func main() {
name, typ, tag, offset = st.Field(1);
assert(typ.String(), "float32");
- //TODO! this is bad - can't put a method in an interface!
- t = reflect.ParseTypeString("", "interface {a int}");
- assert(t.String(), "interface {a int}");
+ t = reflect.ParseTypeString("", "interface {a() *int}");
+ assert(t.String(), "interface {a() *int}");
t = reflect.ParseTypeString("", "*(a int8, b int32)");
assert(t.String(), "*(a int8, b int32)");
+ t = reflect.ParseTypeString("", "*(a int8, b int32) float");
+ assert(t.String(), "*(a int8, b int32) float");
+
+ t = reflect.ParseTypeString("", "*(a int8, b int32) (a float, b float)");
+ assert(t.String(), "*(a int8, b int32) (a float, b float)");
+
t = reflect.ParseTypeString("", "[32]int32");
assert(t.String(), "[32]int32");
at = t.(reflect.ArrayType);
src/lib/reflect/type.go
--- a/src/lib/reflect/type.go
+++ b/src/lib/reflect/type.go
@@ -671,10 +671,10 @@ func (p *Parser) Chan(name string, tokstart, dir int) *StubType {
}
// Parse array of fields for struct, interface, and func arguments
-func (p *Parser) Fields(sep string) *[]Field {
+func (p *Parser) Fields(sep, term string) *[]Field {
a := new([]Field, 10);
nf := 0;
- for p.token != "" && !special(p.token[0]) {
+ for p.token != "" && p.token != term {
if nf == len(a) {
a1 := new([]Field, 2*nf);
for i := 0; i < nf; i++ {
@@ -698,8 +698,16 @@ func (p *Parser) Fields(sep string) *[]Field {
return a[0:nf];
}
+// A single type packaged as a field for a function return
+func (p *Parser) OneField() *[]Field {
+ a := new([]Field, 1);
+ a[0].name = "";
+ a[0].typ = p.Type("");
+ return a;
+}
+
func (p *Parser) Struct(name string, tokstart int) *StubType {
- f := p.Fields(";");
+ f := p.Fields(";", "}");
if p.token != "}" {
return MissingStub;
}
@@ -709,7 +717,7 @@ func (p *Parser) Struct(name string, tokstart int) *StubType {
}
func (p *Parser) Interface(name string, tokstart int) *StubType {
- f := p.Fields(";");
+ f := p.Fields(";", "}");
if p.token != "}" {
return MissingStub;
}
@@ -720,18 +728,24 @@ func (p *Parser) Interface(name string, tokstart int) *StubType {
func (p *Parser) Func(name string, tokstart int) *StubType {
// may be 1 or 2 parenthesized lists
- f1 := NewStructTypeStruct("", "", p.Fields(","));
+ f1 := NewStructTypeStruct("", "", p.Fields(",", ")"));
if p.token != ")" {
return MissingStub;
}
end := p.index;
p.Next();
if p.token != "(" {
- // 1 list: the in parameters only
- return NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, nil));
+ // 1 list: the in parameters are a list. Is there a single out parameter?
+ if p.token == "" || p.token == "}" || p.token == "," || p.token == ";" {
+ return NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, nil));
+ }
+ // A single out parameter.
+ f2 := NewStructTypeStruct("", "", p.OneField());
+ return NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, f2));
+ } else {
+ p.Next();
}
- p.Next();
- f2 := NewStructTypeStruct("", "", p.Fields(","));
+ f2 := NewStructTypeStruct("", "", p.Fields(",", ")"));
if p.token != ")" {
return MissingStub;
}
コアとなるコードの解説
src/lib/reflect/type.go
の変更点
-
Parser.Fields(sep string)
からParser.Fields(sep, term string)
への変更:Fields
メソッドは、構造体、インターフェース、関数の引数/戻り値のリストをパースする汎用的なヘルパー関数です。- 元の実装では、フィールドの終端を判断するために
!special(p.token[0])
という条件を使用していました。これは、特殊文字(括弧など)に遭遇するまでパースを続けるというものでしたが、インターフェースのメソッドシグネチャ内の括弧など、意図しない場所でパースが停止する原因となっていました。 - 新しい
term
引数(ターミネータ)が追加されたことで、パースを終了すべき明確なトークン(例:}
や)
)を指定できるようになりました。これにより、パースロジックがより正確かつ堅牢になりました。
-
Parser.OneField()
メソッドの追加:- この新しいメソッドは、単一の型を関数の戻り値として扱うためのヘルパーです。
- Goの関数は、戻り値が1つの場合でも、複数の場合でも、構文的には異なる扱いをします(例:
func() int
vsfunc() (int, error)
)。 OneField()
は、単一の戻り値をField
スライスとしてラップすることで、Func
メソッドが単一の戻り値と複数の戻り値を統一的に扱えるようにします。
-
Parser.Struct
およびParser.Interface
メソッドの変更:- これらのメソッドは、
Fields
メソッドを呼び出す際に、終端文字として}
を明示的に指定するようになりました (p.Fields(";", "}")
)。 - これにより、構造体やインターフェースの定義が
}
で正しく閉じられていることを確認し、パースの正確性を向上させます。
- これらのメソッドは、
-
Parser.Func
メソッドの変更:- このメソッドは、関数の引数リストと戻り値リストをパースするGoの型システムの中核部分です。
- 引数リストのパース:
p.Fields(",", ")")
のように、引数リストの終端として)
を明示的に指定するようになりました。 - 単一戻り値の処理の改善:
- 元のコードでは、引数リストの後に
(
が続かない場合、戻り値がないものとして扱っていました。 - 修正後、引数リストの後に
(
が続かない場合でも、現在のトークンが空文字列、}
、,
、;
でない限り、それは単一の戻り値であると判断し、新しく追加されたp.OneField()
メソッドを呼び出してその戻り値をパースするようになりました。 - これにより、
func() float
のような単一の戻り値を持つ関数が正しくパースされるようになりました。
- 元のコードでは、引数リストの後に
- 複数戻り値のパース: 複数戻り値のリストも
p.Fields(",", ")")
のように、終端として)
を明示的に指定するようになりました。
src/lib/reflect/test.go
の変更点
- テストケースの追加と修正:
- インターフェースのパースに関する古いコメント
//NOTE: INTERFACES PARSE INCORRECTLY: parser's Fields() stops at '('
が削除されました。これは、バグが修正されたことを示しています。 interface {a int}
のようなインターフェースのテストがinterface {a() *int}
のように、メソッドシグネチャを含むインターフェースのテストに置き換えられました。これは、インターフェースのメソッドパースの修正を検証するためです。- 単一の戻り値を持つ関数型
*(a int8, b int32) float
や、複数の戻り値を持つ関数型*(a int8, b int32) (a float, b float)
のパースを検証する新しいテストケースが追加されました。これにより、Parser.Func
メソッドの修正が正しく機能していることを確認できます。
- インターフェースのパースに関する古いコメント
これらの変更は、Go言語の reflect
パッケージが、Goの型システム、特にインターフェースと関数の型をより正確かつ堅牢にパースできるようにするための重要な改善です。
関連リンク
- Go言語の
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - Go言語のインターフェースに関する公式ブログ記事: https://go.dev/blog/interfaces
- Go言語の関数に関する公式ドキュメント (Go言語仕様): https://go.dev/ref/spec#Function_types
参考にした情報源リンク
- Go言語の初期開発に関する情報 (Goの歴史): https://go.dev/doc/history
- Perforce Change List (CL) の概念に関する一般的な情報 (Google検索結果に基づく): https://www.perforce.com/manuals/v20.1/p4guide/chapter.changelists.html (一般的なPerforceのドキュメントであり、Google内部のシステムに直接関連するものではありませんが、概念理解の助けとなります)
- Go言語の
reflect
パッケージのソースコード (現在のバージョン): https://github.com/golang/go/tree/master/src/reflect (コミット当時のコードとは異なる可能性がありますが、現在の実装を理解する上で参考になります)