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

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

このコミットは、Go言語の初期のreflectパッケージにおける型文字列の生成ロジックに関する修正です。具体的には、構造体のフィールドの型文字列を生成する際に、フィールドに付与されたタグ(struct tag)が誤って型文字列に含まれてしまう問題を解決しています。src/lib/reflect/test.goではこの問題を確認するためのテストケースが追加され、src/lib/reflect/type.goでは型解析器(Parser)が型文字列を正しく抽出するように修正されています。

コミット

reflectパッケージにおいて、構造体フィールドの型文字列からタグを削除する修正。

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

https://github.com/golang/go/commit/5a1cbe8b64fc3e75e2fa4f4c9a74bcca93a1d520

元コミット内容

trim tags from type strings for structure fields.

R=rsc
DELTA=28  (14 added, 4 deleted, 10 changed)
OCL=18561
CL=18563

変更の背景

Go言語のreflectパッケージは、実行時にプログラムの型情報を検査・操作するための機能を提供します。構造体のフィールドには、追加のメタデータとして「タグ」を付与することができます。例えば、JSONエンコーディング/デコーディングの際にフィールド名を指定するためにjson:"field_name"のようなタグが使われます。

このコミットが行われた2008年当時のreflectパッケージの実装では、型文字列をパースして型情報を構築する際に、構造体フィールドの型文字列に誤ってそのフィールドのタグが含まれてしまうバグがありました。これは、型文字列を抽出するロジックが、タグの開始位置と終了位置を正しく認識せず、タグ部分まで含めて型文字列として扱ってしまっていたためと考えられます。

この問題は、型情報を正確に表現する必要があるreflectパッケージの基本的な機能に影響を与えるため、修正が必要でした。特に、Type.String()メソッドが返す値は、その型の正確な文字列表現であるべきであり、タグのようなメタデータは含まれるべきではありません。

前提知識の解説

Go言語のreflectパッケージ

reflectパッケージは、Goプログラムが自身の構造を検査する「リフレクション」機能を提供します。これにより、プログラムは実行時に変数や関数の型、値、メソッドなどを動的に調べたり、操作したりすることができます。

  • reflect.Type: Goの型を表すインターフェースです。reflect.TypeOf(v)関数を使って任意のGoの値vreflect.Typeを取得できます。
  • Type.String(): reflect.Typeインターフェースのメソッドで、その型を文字列表現で返します。例えば、int型なら"int"[]string型なら"[]string"といった文字列が返されます。
  • reflect.StructField: 構造体のフィールドに関する情報(名前、型、タグなど)を保持する構造体です。

構造体タグ (Struct Tags)

Go言語の構造体フィールドには、バッククォート()で囲まれた文字列として「タグ」を付与することができます。このタグは、コンパイル時には無視されますが、reflect`パッケージを使って実行時に読み取ることができます。タグは、主にデータシリアライゼーション(例: JSON、XML)、データベースマッピング、バリデーションなど、様々なメタデータとして利用されます。

例:

type User struct {
    Name string `json:"user_name" validate:"required"`
    Age  int    `json:"user_age"`
}

この例では、Nameフィールドにjson:"user_name"validate:"required"という2つのタグが付与されています。

型文字列のパース

Goのコンパイラや、reflectパッケージのように型情報を動的に扱うライブラリでは、型を文字列表現から解析(パース)する機能が必要になります。このコミットの対象となっているsrc/lib/reflect/type.go内のParser構造体は、まさにこの型文字列のパースを担当していました。パースの過程で、各型の要素(ポインタ、配列、マップ、チャネル、構造体フィールドなど)を正しく識別し、それぞれの型情報を構築する必要があります。

技術的詳細

このコミットの核心は、reflectパッケージ内の型文字列パーサーParserが、構造体フィールドの型を抽出する際に、フィールドに付随するタグを誤って型文字列の一部として含めてしまう問題を修正することにあります。

元の実装では、Parserが型文字列の特定の部分文字列を抽出する際に、トークンの開始位置から現在のパーサーのインデックス(p.index)までの範囲を単純に切り取っていました。しかし、構造体フィールドの定義において、型名の直後にタグが続く場合(例: *[]uint32 "TAG")、p.indexがタグの末尾まで進んでしまい、結果としてタグが型文字列に含まれてしまっていました。

この問題を解決するために、以下の変更が導入されました。

  1. prevendフィールドの追加: Parser構造体にprevendという新しいint型のフィールドが追加されました。このフィールドは、直前にパースされたトークンの終了位置(の次の位置)を記録するために使用されます。
  2. Next()メソッドの変更: Parser.Next()メソッド(次のトークンを読み込むメソッド)の冒頭で、p.prevend = p.index;という行が追加されました。これにより、新しいトークンを読み込む前に、現在のp.index(つまり、直前のトークンの終了位置)がprevendに保存されるようになります。
  3. TypeString(i int) stringメソッドの追加: ParserTypeStringというヘルパーメソッドが追加されました。このメソッドは、型文字列の開始位置iを受け取り、p.str[i:p.prevend]というスライス操作で型文字列を返します。これにより、型文字列の抽出範囲が、現在のトークン(タグ)の開始位置ではなく、直前のトークン(型名)の終了位置(prevend)までとなるため、タグが型文字列に含まれるのを防ぎます。
  4. 型構築関数の修正: Array, Map, Chan, Struct, Interface, Func, Ptrなどの型を構築するメソッド内で、型文字列を渡す際にp.str[tokstart:p.index]の代わりに新しく追加されたp.TypeString(tokstart)を使用するように変更されました。これにより、タグが型文字列から除外されるようになります。

特に、src/lib/reflect/test.goに追加されたテストケースは、この問題の具体的なシナリオを示しています。 t = reflect.ParseTypeString("", "struct{d *[]uint32 \"TAG\"}"); この行では、*[]uint32という型に"TAG"というタグが付与された構造体フィールドを定義しています。修正前は、typ.String()"*[]uint32 \"TAG\""のような文字列を返していましたが、修正後は"*[]uint32"という正しい型文字列を返すようになります。

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

src/lib/reflect/test.go

--- a/src/lib/reflect/test.go
+++ b/src/lib/reflect/test.go
@@ -250,4 +250,10 @@ func main() {
 	assert(t.String(), "chan<-string");
 	ct = t.(reflect.ChanType);
 	assert(ct.Elem().String(), "string");
+
+	// make sure tag strings are not part of element type
+	t = reflect.ParseTypeString("", "struct{d *[]uint32 \"TAG\"}");
+	st = t.(reflect.StructType);
+	name, typ, tag, offset = st.Field(0);
+	assert(typ.String(), "*[]uint32");
 }

src/lib/reflect/type.go

--- a/src/lib/reflect/type.go
+++ b/src/lib/reflect/type.go
@@ -557,11 +557,19 @@ type Parser struct {
 	str	string;	// string being parsed
 	token	string;	// the token being parsed now
 	tokstart	int;	// starting position of token
+	prevend	int;	// (one after) ending position of previous token
 	index	int;	// next character position in str
 }
 
+// Return typestring starting at position i.
+// Trim trailing blanks.
+func (p *Parser) TypeString(i int) string {
+	return p.str[i:p.prevend];
+}
+
 // Load next token into p.token
 func (p *Parser) Next() {
+	p.prevend = p.index;
 	token := "";
 	for ; p.index < len(p.str) && p.str[p.index] == ' '; p.index++ {
 	}
@@ -643,7 +651,7 @@ func (p *Parser) Array(name string, tokstart int) *StubType {
 	}\n \tp.Next();
 \telemtype := p.Type("");
-\treturn NewStubType(name, NewArrayTypeStruct(name, p.str[tokstart:p.index], open, size, elemtype));
+\treturn NewStubType(name, NewArrayTypeStruct(name, p.TypeString(tokstart), open, size, elemtype));
 }\n \n func (p *Parser) Map(name string, tokstart int) *StubType {
@@ -657,7 +665,7 @@ func (p *Parser) Map(name string, tokstart int) *StubType {
 	}\n \tp.Next();
 \telemtype := p.Type("");
-\treturn NewStubType(name, NewMapTypeStruct(name, p.str[tokstart:p.index], keytype, elemtype));
+\treturn NewStubType(name, NewMapTypeStruct(name, p.TypeString(tokstart), keytype, elemtype));
 }\n \n func (p *Parser) Chan(name string, tokstart, dir int) *StubType {
@@ -669,7 +677,7 @@ func (p *Parser) Chan(name string, tokstart, dir int) *StubType {
 	\tdir = SendDir;
 \t}\n \telemtype := p.Type("");
-\treturn NewStubType(name, NewChanTypeStruct(name, p.str[tokstart:p.index], dir, elemtype));
+\treturn NewStubType(name, NewChanTypeStruct(name, p.TypeString(tokstart), dir, elemtype));
 }\n \n // Parse array of fields for struct, interface, and func arguments
@@ -713,9 +721,8 @@ func (p *Parser) Struct(name string, tokstart int) *StubType {
 \tif p.token != "}" {
 \t\treturn MissingStub;
 \t}\n-\tts := p.str[tokstart:p.index];
 \tp.Next();
-\treturn NewStubType(name, NewStructTypeStruct(name, ts, f));
+\treturn NewStubType(name, NewStructTypeStruct(name, p.TypeString(tokstart), f));
 }\n \n func (p *Parser) Interface(name string, tokstart int) *StubType {
@@ -723,9 +730,8 @@ func (p *Parser) Interface(name string, tokstart int) *StubType {
 \tif p.token != "}" {
 \t\treturn MissingStub;
 \t}\n-\tts := p.str[tokstart:p.index];
 \tp.Next();
-\treturn NewStubType(name, NewInterfaceTypeStruct(name, ts, f));
+\treturn NewStubType(name, NewInterfaceTypeStruct(name, p.TypeString(tokstart), f));
 }\n \n func (p *Parser) Func(name string, tokstart int) *StubType {
@@ -734,16 +740,15 @@ func (p *Parser) Func(name string, tokstart int) *StubType {
 \tif p.token != ")" {
 \t\treturn MissingStub;
 \t}\n-\tend := p.index;
 \tp.Next();
 \tif p.token != "(" {
 \t\t// 1 list: the in parameters are a list.  Is there a single out parameter?
 \t\tif p.token == "" || p.token == "}" || p.token == "," || p.token == ";" {
-\t\t\treturn NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, nil));
+\t\t\treturn NewStubType(name, NewFuncTypeStruct(name, p.TypeString(tokstart), f1, nil));
 \t\t}\n \t\t// A single out parameter.
 \t\tf2 := NewStructTypeStruct("", "", p.OneField());
-\t\treturn NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, f2));
+\t\treturn NewStubType(name, NewFuncTypeStruct(name, p.TypeString(tokstart), f1, f2));
 \t} else {
 \t\tp.Next();
 \t}\n@@ -751,10 +756,9 @@ func (p *Parser) Func(name string, tokstart int) *StubType {
 \tif p.token != ")" {
 \t\treturn MissingStub;
 \t}\n-\tend = p.index;
 \tp.Next();
 \t// 2 lists: the in and out parameters are present
-\treturn NewStubType(name, NewFuncTypeStruct(name, p.str[tokstart:end], f1, f2));
+\treturn NewStubType(name, NewFuncTypeStruct(name, p.TypeString(tokstart), f1, f2));
 }\n \n func (p *Parser) Type(name string) *StubType {
@@ -766,7 +770,7 @@ func (p *Parser) Type(name string) *StubType {
 \tcase p.token == "*":
 \t\tp.Next();
 \t\tsub := p.Type("");
-\t\treturn NewStubType(name, NewPtrTypeStruct(name, p.str[tokstart:p.index], sub));
+\t\treturn NewStubType(name, NewPtrTypeStruct(name, p.TypeString(tokstart), sub));
 \tcase p.token == "[":
 \t\tp.Next();
 \t\treturn p.Array(name, tokstart);

コアとなるコードの解説

src/lib/reflect/test.goの変更

  • 新しいテストケースがmain関数内に追加されました。
  • t = reflect.ParseTypeString("", "struct{d *[]uint32 \"TAG\"}");
    • この行は、dという名前のフィールドを持ち、その型が*[]uint32で、"TAG"というタグが付与された構造体を定義する型文字列をパースしています。
  • st = t.(reflect.StructType);
    • パースされた型をreflect.StructTypeに型アサートしています。
  • name, typ, tag, offset = st.Field(0);
    • 構造体の最初のフィールド(d)の情報を取得しています。
  • assert(typ.String(), "*[]uint32");
    • この行が重要です。 フィールドdの型(typ)の文字列表現が、タグを含まない"*[]uint32"であることをアサートしています。修正前はこのアサートが失敗していました。

src/lib/reflect/type.goの変更

  • Parser構造体へのprevendフィールドの追加:

    type Parser struct {
        // ... 既存のフィールド ...
        prevend int;    // (one after) ending position of previous token
        // ... 既存のフィールド ...
    }
    

    prevendは、直前に処理されたトークンの終了位置を記憶するためのものです。これにより、現在のトークン(例えばタグ)が始まる前の、純粋な型文字列の終わりを正確に特定できるようになります。

  • Parser.TypeString(i int) stringメソッドの追加:

    func (p *Parser) TypeString(i int) string {
        return p.str[i:p.prevend];
    }
    

    この新しいヘルパーメソッドは、型文字列の開始インデックスiを受け取り、p.str(パース中の文字列全体)からiからp.prevendまでの部分文字列を切り出して返します。これにより、型文字列の末尾がp.prevendで正確に区切られ、タグが含まれることがなくなります。

  • Parser.Next()メソッドの変更:

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -566,6 +566,7 @@ func (p *Parser) Next() {
     
     // Load next token into p.token
     func (p *Parser) Next() {
    +    p.prevend = p.index;
         token := "";
         // ...
     }
    

    Next()メソッドが呼び出されるたびに、新しいトークンを読み込む前に、現在のp.index(つまり、直前のトークンの終了位置)がp.prevendに保存されます。これにより、TypeStringメソッドが常に正しい範囲を参照できるようになります。

  • 型構築関数でのTypeStringの利用: Array, Map, Chan, Struct, Interface, Func, Ptrといった様々な型を構築するメソッド内で、型文字列を生成する際に、これまではp.str[tokstart:p.index]のように直接スライスしていましたが、これをp.TypeString(tokstart)に置き換えました。 例えば、Array型の構築部分では、

    --- a/src/lib/reflect/type.go
    +++ b/src/lib/reflect/type.go
    @@ -643,7 +651,7 @@ func (p *Parser) Array(name string, tokstart int) *StubType {
     	}\n \tp.Next();
     \telemtype := p.Type("");
    -\treturn NewStubType(name, NewArrayTypeStruct(name, p.str[tokstart:p.index], open, size, elemtype));
    +\treturn NewStubType(name, NewArrayTypeStruct(name, p.TypeString(tokstart), open, size, elemtype));
     }
    

    このように変更することで、型文字列の抽出がprevendによって制御され、タグが意図せず含まれることがなくなりました。

これらの変更により、reflectパッケージは構造体フィールドの型文字列を正確に表現できるようになり、タグが型情報の一部として誤って解釈される問題が解決されました。

関連リンク

参考にした情報源リンク

  • Go言語のreflectパッケージの概念と使用法に関する一般的な情報源
  • Go言語の構造体タグに関する一般的な情報源
  • Go言語の初期のコミット履歴と設計思想に関する情報源 (特に2008年当時のGoの設計に関する議論)
  • GitHubのコミットページ: https://github.com/golang/go/commit/5a1cbe8b64fc3e75e2fa4f4c9a74bcca93a1d520
  • Go言語のソースコード(特にsrc/reflectディレクトリ)