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

[インデックス 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の型の抽象表現です。例えば、intstringstructinterfacefunc などの型情報を保持します。
  • 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))の区別とパースが重要です。

OCLCL

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.FuncOneField メソッドを活用し、単一の戻り値を持つ関数を正しくパースできるようになりました。

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.StructParser.Interface メソッドの変更

Parser.StructParser.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 の変更点

  1. Parser.Fields(sep string) から Parser.Fields(sep, term string) への変更:

    • Fields メソッドは、構造体、インターフェース、関数の引数/戻り値のリストをパースする汎用的なヘルパー関数です。
    • 元の実装では、フィールドの終端を判断するために !special(p.token[0]) という条件を使用していました。これは、特殊文字(括弧など)に遭遇するまでパースを続けるというものでしたが、インターフェースのメソッドシグネチャ内の括弧など、意図しない場所でパースが停止する原因となっていました。
    • 新しい term 引数(ターミネータ)が追加されたことで、パースを終了すべき明確なトークン(例: }))を指定できるようになりました。これにより、パースロジックがより正確かつ堅牢になりました。
  2. Parser.OneField() メソッドの追加:

    • この新しいメソッドは、単一の型を関数の戻り値として扱うためのヘルパーです。
    • Goの関数は、戻り値が1つの場合でも、複数の場合でも、構文的には異なる扱いをします(例: func() int vs func() (int, error))。
    • OneField() は、単一の戻り値を Field スライスとしてラップすることで、Func メソッドが単一の戻り値と複数の戻り値を統一的に扱えるようにします。
  3. Parser.Struct および Parser.Interface メソッドの変更:

    • これらのメソッドは、Fields メソッドを呼び出す際に、終端文字として } を明示的に指定するようになりました (p.Fields(";", "}"))。
    • これにより、構造体やインターフェースの定義が } で正しく閉じられていることを確認し、パースの正確性を向上させます。
  4. 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の型システム、特にインターフェースと関数の型をより正確かつ堅牢にパースできるようにするための重要な改善です。

関連リンク

参考にした情報源リンク