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

[インデックス 1053] ファイルの概要

このコミットは、Go言語のreflect(リフレクション)ライブラリにおいて、型情報を文字列としてパースし、また型オブジェクトから文字列表現を取得する機能を追加するものです。これにより、Goプログラムが実行時に自身の型情報をより柔軟に操作できるようになります。

コミット

commit a45f947c34004bca8002b85a13cfe1902a4f89c8
Author: Rob Pike <r@golang.org>
Date:   Tue Nov 4 22:54:11 2008 -0800

    type strings through the reflection library.
    
    R=rsc
    DELTA=187  (107 added, 28 deleted, 52 changed)
    OCL=18510
    CL=18510

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a45f947c34004bca8002b85a13cfe1902a4f89c8

元コミット内容

このコミットの目的は、Goのリフレクションライブラリを通じて型文字列を処理できるようにすることです。具体的には、型を表す文字列(例: *int8, map[string]*int32, struct {c *chan *int32; d float32})をreflect.Typeオブジェクトに変換する機能と、その逆の変換(reflect.Typeオブジェクトから型文字列を取得する機能)を導入しています。

変更の背景

Go言語は静的型付け言語ですが、リフレクションは実行時にプログラムの構造(型、フィールド、メソッドなど)を検査・操作するための強力な機能を提供します。このコミット以前は、reflectパッケージは主に既存のGo値から型情報を抽出することに焦点を当てていました。しかし、動的に型を生成したり、外部から与えられた型定義文字列に基づいて処理を行ったりするような高度なシナリオでは、型文字列を直接パースする機能が不可欠となります。

この機能が追加されることで、以下のようなユースケースが考えられます。

  1. 動的なコード生成: 型定義を文字列として受け取り、それに基づいてコードを生成するツールやライブラリの開発。
  2. シリアライゼーション/デシリアライゼーション: ネットワーク経由で型情報を含むデータを送受信する際に、型文字列を効率的に扱う。
  3. プラグインシステム: 実行時にロードされるモジュールが、文字列で指定された型に基づいて動作を調整する。
  4. REPL (Read-Eval-Print Loop): 対話型シェルでユーザーが入力した型定義を即座に解釈し、操作する。

この変更は、Goのリフレクションシステムをより強力で柔軟なものにするための基礎的なステップと言えます。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念が役立ちます。

  1. リフレクション (Reflection): プログラミング言語が自身の構造(型、変数、関数など)を実行時に検査・操作する能力のことです。Go言語では、reflectパッケージがこの機能を提供します。これにより、コンパイル時には未知の型や構造を持つデータを扱うことが可能になります。

  2. Goの型システム: Goは静的型付け言語であり、すべての変数にはコンパイル時に型が決定されます。Goには、プリミティブ型(int, string, boolなど)、複合型(struct, array, slice, map, chan)、関数型、インターフェース型、ポインタ型など、様々な種類の型が存在します。

  3. reflect.Type: Goのreflectパッケージにおける中心的なインターフェースの一つで、Goの型のメタデータを表現します。reflect.TypeOf(v)関数を使うことで、任意のGoの値vreflect.Typeを取得できます。reflect.Typeは、型の種類(Kind())、名前(Name())、サイズ(Size())などの情報を提供します。

  4. reflect.Value: reflectパッケージのもう一つの中心的なインターフェースで、Goの実行時の値を表現します。reflect.ValueOf(v)関数を使うことで、任意のGoの値vreflect.Valueを取得できます。reflect.Valueを通じて、値の読み書きやメソッドの呼び出しなどを行うことができます。

  5. パーサー (Parser): 文字列やトークンのシーケンスを入力として受け取り、それらを構造化されたデータ(この場合はGoの型を表す抽象構文木のようなもの)に変換するプログラムまたはルーチンです。このコミットでは、Goの型定義文字列を解析するためのパーサーがreflectパッケージ内に実装されています。

  6. 抽象構文木 (Abstract Syntax Tree, AST): ソースコードの抽象的な構文構造を木構造で表現したものです。パーサーは通常、入力文字列をASTに変換します。このコミットでは、型文字列をパースしてreflect.Typeオブジェクトの内部表現を構築していると考えることができます。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. reflect.Typeインターフェースの拡張:

    • String() string メソッドが追加されました。これにより、任意のreflect.Typeオブジェクトが自身の型定義を文字列として返すことができるようになります。
    • SetString(string) メソッドも追加されましたが、コメントに// TODO: remove when no longer neededとあり、一時的な内部利用のためのものであることが示唆されています。
  2. Common構造体の変更:

    • reflectパッケージ内のすべての具体的な型構造体(PtrTypeStruct, ArrayTypeStructなど)に埋め込まれているCommon構造体に、str stringフィールドが追加されました。このフィールドが、その型の文字列表現を保持します。
    • Common構造体にもString()SetString()メソッドが実装され、reflect.Typeインターフェースの要件を満たしています。
  3. 型構造体のコンストラクタの変更:

    • NewPtrTypeStruct, NewArrayTypeStruct, NewMapTypeStructなど、各種型構造体のコンストラクタ関数に、typestring stringという新しい引数が追加されました。
    • このtypestringは、パースされた型文字列そのものであり、Common構造体のstrフィールドに格納されます。これにより、型オブジェクトが自身の文字列表現を内部に持つことができるようになりました。
  4. 型文字列パーサーの実装と強化:

    • src/lib/reflect/type.go内のParser構造体と関連するメソッド(Array, Map, Chan, Struct, Interface, Func, Type)が大幅に修正・拡張されました。
    • Parser構造体にtokstart intフィールドが追加され、現在パース中のトークンの開始位置を記録できるようになりました。これは、パースされた型文字列の正確なサブストリングを抽出するために使用されます。
    • 各型をパースするメソッド(例: p.Array(name, tokstart))は、パース対象の型文字列の開始位置tokstartを受け取り、パースが完了した時点でのp.index(現在のパーサーの位置)までの文字列p.str[tokstart:p.index]を抽出します。この抽出された文字列が、対応するNew*TypeStruct関数にtypestringとして渡されます。
    • これにより、reflect.ParseTypeString関数(src/lib/reflect/test.goでテストされている)が、任意のGo型文字列を正確にパースし、対応するreflect.Typeオブジェクトを生成できるようになりました。
  5. reflect.ValueUnreflect()メソッド:

    • src/lib/reflect/value.goにおいて、ValueインターフェースにUnreflect() Emptyメソッドが追加されました。
    • このメソッドは、reflect.Valueオブジェクトを、それがラップしている元のGoのインターフェース値(Emptyインターフェースとして表現される)に戻すことを可能にします。
    • 実装では、sys.unreflect(*AddrToPtrAddr(c.addr), c.typ.String())が呼び出されており、値のアドレスと型文字列(c.typ.String()で取得)を使用して、元の値への変換を行っていることがわかります。これは、リフレクションで取得した値を元の型に戻す「ラウンドトリップ」機能の重要な部分です。

これらの変更により、Goのリフレクションシステムは、型情報を文字列として扱い、それをパースして型オブジェクトを生成し、さらに型オブジェクトから文字列表現を再構築する能力を獲得しました。

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

src/lib/reflect/type.go

  1. Type インターフェースの定義変更:

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -50,22 +50,33 @@ var DotDotDotString = "..."
     export type Type interface {
      	Kind()	int;
      	Name()	string;
    +	String()	string;
    +	SetString(string);	// TODO: remove when no longer needed
      	Size()	uint64;
      }
    

    String()SetString()メソッドが追加されました。

  2. Common 構造体の定義とメソッドの追加:

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -59,7 +59,7 @@ type Common struct {
      }
      
      func NewBasicType(name string, kind int, size uint64) Type {
    -	return &BasicType{ Common{kind, name, size} }
    +	return &BasicType{ Common{kind, name, name, size} }
      }
    

    strフィールドが追加され、String()SetString()メソッドが実装されました。NewBasicTypeの初期化でnamestrフィールドにも渡されるようになりました。

  3. 各種 New*TypeStruct 関数のシグネチャ変更: 例: NewPtrTypeStruct

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -134,8 +145,8 @@ type PtrTypeStruct struct {
      	sub	*StubType;
      }
      
    -func NewPtrTypeStruct(name string, sub *StubType) *PtrTypeStruct {
    -	return &PtrTypeStruct{ Common{PtrKind, name, ptrsize}, sub}
    +func NewPtrTypeStruct(name, typestring string, sub *StubType) *PtrTypeStruct {
    +	return &PtrTypeStruct{ Common{PtrKind, typestring, name, ptrsize}, sub}
      }
    

    typestring引数が追加され、Common構造体の初期化に利用されています。他のNewArrayTypeStruct, NewMapTypeStruct, NewChanTypeStruct, NewStructTypeStruct, NewInterfaceTypeStruct, NewFuncTypeStructも同様に変更されています。

  4. Parser 構造体とパースメソッドの変更:

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -543,6 +554,7 @@ func unescape(s string, backslash bool) string {
     type Parser struct {
      	str	string;	// string being parsed
      	token	string;	// the token being parsed now
    +	tokstart	int;	// starting position of token
      	index	int;	// next character position in str
      }
    

    tokstartフィールドが追加されました。

    例: Parser.Array メソッドのシグネチャと実装変更

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -608,7 +621,7 @@ func (p *Parser) Next() {
      
      func (p *Parser) Type(name string) *StubType
      
    -func (p *Parser) Array(name string) *StubType {
    +func (p *Parser) Array(name string, tokstart int) *StubType {
      	size := uint64(0);
      	open := true;
      	if p.token != "]" {
    @@ -628,10 +641,10 @@ func (p *Parser) Array(name string) *StubType {
      	}
      	p.Next();
      	elemtype := p.Type("");
    -	return NewStubType(name, NewArrayTypeStruct(name, open, size, elemtype));
    +	return NewStubType(name, NewArrayTypeStruct(name, p.str[tokstart:p.index], open, size, elemtype));
      }
    

    tokstart引数が追加され、p.str[tokstart:p.index]で型文字列を抽出し、NewArrayTypeStructに渡しています。他のパースメソッドも同様に変更されています。

src/lib/reflect/value.go

  1. Value インターフェースの定義変更:

    --- a/src/lib/reflect/value.go
    +++ b/src/lib/reflect/value.go
    @@ -11,12 +11,35 @@ import (
      	"reflect";
      )
      
    -
     type Addr uint64	// TODO: where are ptrint/intptr etc?
      
    +// Conversion functions, implemented in assembler
    +func AddrToPtrAddr(Addr) *Addr
    +func AddrToPtrInt(Addr) *int
    +// ... (他の AddrToPtr* 関数)
    +
    +export type Empty interface {}	// TODO(r): Delete when no longer needed?
    +
     export type Value interface {
      	Kind()	int;
      	Type()	Type;
    +	Unreflect()	Empty;
      }
    

    Unreflect()メソッドが追加されました。

  2. Common 構造体への Unreflect() メソッドの実装:

    --- a/src/lib/reflect/value.go
    +++ b/src/lib/reflect/value.go
    @@ -35,30 +58,14 @@ func (c *Common) Type() Type {
      	return c.typ
      }
      
    +func (c *Common) Unreflect() Empty {
    +	return sys.unreflect(*AddrToPtrAddr(c.addr), c.typ.String());
    +}
    +
     func NewValueAddr(typ Type, addr Addr) Value
      
     type Creator *(typ Type, addr Addr) Value
    

    Unreflect()の実装が追加され、sys.unreflectを呼び出す際にc.typ.String()(型文字列)を使用しています。

src/lib/reflect/test.go

  1. 型文字列パースのテストケース追加:
    --- a/src/lib/reflect/test.go
    +++ b/src/lib/reflect/test.go
    @@ -191,4 +194,56 @@ func main() {
      	\t\tvalue.(reflect.PtrValue).Sub().(reflect.ArrayValue).Elem(4).(reflect.IntValue).Put(123);
      	\t\tassert(reflect.ValueToString(value.(reflect.PtrValue).Sub()), "main.AA·test{1, 2, 3, 4, 123, 6, 7, 8, 9, 10}");
      	}
    +\n+\tvar pt reflect.PtrType;
    +\tvar st reflect.StructType;
    +\tvar mt reflect.MapType;
    +\tvar at reflect.ArrayType;
    +\tvar ct reflect.ChanType;
    +\tvar name string;
    +\tvar typ reflect.Type;
    +\tvar tag string;
    +\tvar offset uint64;
    +\n+\t// Type strings
    +\tt = reflect.ParseTypeString("", "int8");
    +\tassert(t.String(), "int8");
    +\n+\tt = reflect.ParseTypeString("", "*int8");
    +\tassert(t.String(), "*int8");
    +\tpt = t.(reflect.PtrType);
    +\tassert(pt.Sub().String(), "int8");
    +\n+\tt = reflect.ParseTypeString("", "*struct {c *chan *int32; d float32}");
    +\tassert(t.String(), "*struct {c *chan *int32; d float32}");
    +\tpt = t.(reflect.PtrType);
    +\tassert(pt.Sub().String(), "struct {c *chan *int32; d float32}");
    +\tst = pt.Sub().(reflect.StructType);
    +\tname, typ, tag, offset = st.Field(0);
    +\tassert(typ.String(), "*chan *int32");
    +\tname, typ, tag, offset = st.Field(1);
    +\tassert(typ.String(), "float32");
    +\n+\t//TODO! this is bad - can't put a method in an interface!
    +\tt = reflect.ParseTypeString("", "interface {a int}");
    +\tassert(t.String(), "interface {a int}");
    +\n+\tt = reflect.ParseTypeString("", "*(a int8, b int32)");
    +\tassert(t.String(), "*(a int8, b int32)");
    +\n+\tt = reflect.ParseTypeString("", "[32]int32");
    +\tassert(t.String(), "[32]int32");
    +\tat = t.(reflect.ArrayType);
    +\tassert(at.Elem().String(), "int32");
    +\n+\tt = reflect.ParseTypeString("", "map[string]*int32");
    +\tassert(t.String(), "map[string]*int32");
    +\tmt = t.(reflect.MapType);
    +\tassert(mt.Key().String(), "string");
    +\tassert(mt.Elem().String(), "*int32");
    +\n+\tt = reflect.ParseTypeString("", "chan<-string");
    +\tassert(t.String(), "chan<-string");
    +\tct = t.(reflect.ChanType);
    +\tassert(ct.Elem().String(), "string");
      }
    
    reflect.ParseTypeString関数を使用して様々な型文字列をパースし、その結果のreflect.TypeオブジェクトのString()メソッドが期待通りの文字列を返すことを検証しています。また、ポインタ、構造体、マップ、配列、チャネルなどのサブ要素の型も正しく取得できることを確認しています。

コアとなるコードの解説

このコミットの核心は、Goの型システムとリフレクションの間のギャップを埋めることにあります。これまでは、Goのソースコードに書かれた型定義はコンパイル時に内部的な型オブジェクトに変換され、リフレクションはその型オブジェクトを検査するものでした。しかし、このコミットにより、Goの型定義を表現する文字列が、リフレクションシステム内で第一級の市民として扱われるようになりました。

具体的には、以下のメカニズムが導入されました。

  1. 型文字列の内部保持: 各reflect.Typeオブジェクト(の基底となるCommon構造体)が、自身の型定義の文字列表現を直接保持するようになりました。これにより、Type.String()メソッドを呼び出すだけで、その型の正確な文字列表現を簡単に取得できます。これは、型オブジェクトが「自分がどのような文字列から生成されたか」を記憶しているようなものです。

  2. 型文字列のパース: reflectパッケージ内に、Goの型定義文字列を解析し、対応するreflect.Typeオブジェクトの階層構造を構築する本格的なパーサーが実装されました。このパーサーは、ポインタ、配列、マップ、チャネル、構造体、インターフェース、関数といったGoの複雑な型構文を理解し、それらを適切なreflect.Typeのサブタイプにマッピングします。Parser構造体のtokstartフィールドと、各パースメソッドでのp.str[tokstart:p.index]による文字列抽出は、パースされた部分の正確な文字列表現をキャプチャし、それを対応する型オブジェクトに格納するために不可欠です。

  3. Unreflect()による値の再構築: reflect.ValueUnreflect()メソッドが追加されたことで、リフレクションによって操作された値を、その型文字列情報に基づいて元のGoのインターフェース値に戻すことが可能になりました。これは、リフレクションが単なる検査ツールではなく、動的な値の生成や変換にも利用できることを示唆しています。

これらの変更は、Goのリフレクションがより強力なメタプログラミングツールへと進化するための重要な一歩であり、Go言語の動的な側面を拡張する上で基盤となる機能です。

関連リンク

参考にした情報源リンク