[インデックス 15981] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージにおけるバグ修正に関するものです。具体的には、テンプレート内で関数呼び出しから始まるチェーン(例: {{func.Field}}
)が正しく評価されない問題を修正しています。
コミット
commit 99645db9268cfe93f561ef2de013ea5f58304c79
Author: Rob Pike <r@golang.org>
Date: Wed Mar 27 16:31:14 2013 -0700
text/template: fix bug in evaluating a chain starting with a function.
R=golang-dev, alberto.garcia.hierro
CC=golang-dev
https://golang.org/cl/7861046
---
src/pkg/text/template/exec.go | 2 ++\n src/pkg/text/template/exec_test.go | 27 +++++++++++++++++----------
2 files changed, 19 insertions(+), 10 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/99645db9268cfe93f561ef2de013ea5f58304c79
元コミット内容
このコミットは、text/template
パッケージにおいて、関数から始まるチェーンの評価に関するバグを修正します。具体的には、{{func.Field}}
のような形式で、関数が返す値のフィールドにアクセスしようとした際に、正しく評価されない問題がありました。この修正により、このようなテンプレート構文が期待通りに動作するようになります。
変更の背景
Go言語の text/template
パッケージは、テキストベースの出力を生成するための強力なツールです。テンプレートは、データ構造と組み合わせて使用され、動的なコンテンツを生成します。テンプレート内では、変数、フィールド、メソッド、関数などを参照し、それらをチェーンして複雑な式を構築することができます。
このコミットが修正するバグは、特に「関数呼び出しの結果」に対してさらにフィールドアクセスを行うようなケースで発生していました。例えば、mapOfThree
という関数が map[string]int{"three": 3}
を返す場合、テンプレート内で {{mapOfThree.three}}
と記述しても、期待される 3
という値が得られないという問題がありました。これは、テンプレートエンジンの評価ロジックが、関数から始まるチェーンを適切に処理できていなかったためです。
この問題は、テンプレートの柔軟性を損ない、開発者が意図した通りの動的なコンテンツを生成することを妨げていました。そのため、このバグの修正は、text/template
パッケージの堅牢性と使いやすさを向上させる上で重要でした。
前提知識の解説
Go言語の text/template
パッケージ
text/template
パッケージは、Go言語でテキストベースのテンプレートを扱うための標準ライブラリです。HTML、XML、プレーンテキストなど、様々な形式のテキストを生成するのに使用されます。主な特徴は以下の通りです。
- データ駆動: テンプレートはGoのデータ構造(構造体、マップ、スライスなど)と組み合わせて使用されます。
- アクション: テンプレート内では、
{{...}}
で囲まれた「アクション」と呼ばれる特殊な構文を使って、データの表示、条件分岐、繰り返し処理、関数呼び出しなどを行います。 - パイプライン: アクション内では、Unixのパイプラインのように
|
を使って複数のコマンドを連結できます。例えば、{{.Name | upper}}
は.Name
の値をupper
関数に渡します。 - コンテキスト (
.
): テンプレートの評価中に、現在のデータコンテキストは.
(ドット) で表されます。例えば、構造体のフィールドにアクセスするには{{.FieldName}}
と記述します。 - 関数: テンプレート内でGoの関数を呼び出すことができます。これらの関数は
FuncMap
を介してテンプレートに登録されます。
Go言語の reflect
パッケージ
reflect
パッケージは、Goプログラムの実行時に型情報を検査し、値を動的に操作するための機能を提供します。text/template
のような動的な評価を行うライブラリでは、この reflect
パッケージが内部的に広く利用されています。
reflect.Value
: 任意のGoの値を表します。この型を通じて、値の型、フィールド、メソッドなどにアクセスできます。reflect.Type
: Goの型情報を表します。- 動的な呼び出し:
reflect.Value
を使用して、実行時にメソッドや関数を呼び出すことができます。
text/template
は、テンプレート内で指定された変数名や関数名に対応するGoのデータや関数を、reflect
パッケージを使って動的に探し出し、評価します。
テンプレートの評価とチェーン
text/template
は、テンプレートをパース(構文解析)して抽象構文木(AST)を構築し、そのASTをトラバースしながら評価(実行)します。
テンプレート内の式は、しばしば「チェーン」として評価されます。例えば、{{.User.Address.Street}}
のような式は、現在のコンテキスト (.
) から User
フィールドを取得し、その User
の値から Address
フィールドを取得し、さらにその Address
の値から Street
フィールドを取得するという一連の操作を意味します。
このコミットで問題となっていたのは、このチェーンの最初の要素が「関数呼び出し」である場合でした。通常、{{.Field}}
や {{.Method}}
のように、コンテキスト (.
) から直接フィールドやメソッドにアクセスするチェーンは正しく機能します。しかし、{{funcName.Field}}
のように、まず funcName
を呼び出し、その戻り値に対して Field
にアクセスするようなケースで、評価ロジックに不備がありました。
技術的詳細
text/template
パッケージの内部では、テンプレートの実行は exec.go
ファイル内の state
構造体と関連するメソッドによって管理されます。特に、evalArg
メソッドは、テンプレート内の引数ノード(parse.Node
)を評価し、その結果を reflect.Value
として返します。
バグが発生していたのは、evalArg
メソッドが parse.IdentifierNode
型のノードを処理する際のロジックでした。parse.IdentifierNode
は、変数名、関数名、またはフィールド名を表すノードです。
修正前のコードでは、evalArg
メソッド内で parse.IdentifierNode
が現れた場合、そのノードが変数として評価されるか、または特定の型に変換されるケースのみが考慮されていました。しかし、この識別子が「関数」であり、その関数の戻り値に対してさらにフィールドアクセス(チェーン)が行われるシナリオが適切に処理されていませんでした。
具体的には、evalArg
メソッドの switch n := n.(type)
ブロック内で、*parse.IdentifierNode
のケースが欠落していました。これにより、{{mapOfThree.three}}
のような式が評価される際、mapOfThree
が parse.IdentifierNode
として認識されても、それが関数として呼び出され、その結果が次のチェーンの評価に渡されるという処理パスがありませんでした。
このコミットでは、evalArg
メソッドに case *parse.IdentifierNode:
の処理が追加されました。この新しいケースでは、識別子ノードが関数として評価され(s.evalFunction
を呼び出す)、その結果が typ
に従って検証されます。これにより、関数呼び出しから始まるチェーンが正しく評価されるようになりました。
s.evalFunction(dot, arg, arg, nil, zero)
の呼び出しは、arg
が関数名を表す IdentifierNode
であることを示しています。dot
は現在のコンテキスト、arg
は関数名、nil
と zero
は引数がないことを示しています。この呼び出しによって、mapOfThree
関数が実行され、その戻り値(map[string]int{"three": 3}
)が evalArg
の結果として返され、その後の .three
の評価に利用されるようになります。
コアとなるコードの変更箇所
src/pkg/text/template/exec.go
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -619,6 +619,8 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle
return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ)
case *parse.PipeNode:
return s.validateType(s.evalPipeline(dot, arg), typ)
+ case *parse.IdentifierNode:
+ return s.evalFunction(dot, arg, arg, nil, zero)
}
switch typ.Kind() {
case reflect.Bool:
src/pkg/text/template/exec_test.go
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -499,6 +499,8 @@ var execTests = []execTest{\n {\"bug8b\", \"{{4|dddArg 3}}\", \"\", tVal, false},\n // A bug was introduced that broke map lookups for lower-case names.\n {\"bug9\", \"{{.cause}}\", \"neglect\", map[string]string{\"cause\": \"neglect\"}, true},\n+\t// Field chain starting with function did not work.\n+\t{\"bug10\", \"{{mapOfThree.three}}-{{(mapOfThree).three}}\", \"3-3\", 0, true},\n }\n \n func zeroArgs() string {\
@@ -560,19 +562,24 @@ func stringer(s fmt.Stringer) string {\
return s.String()\n }\n \n+func mapOfThree() interface{} {\
+\treturn map[string]int{\"three\": 3}\
+}\n+\n func testExecute(execTests []execTest, template *Template, t *testing.T) {\
\tb := new(bytes.Buffer)\n \tfuncs := FuncMap{\n-\t\t\"add\": add,\n-\t\t\"count\": count,\n-\t\t\"dddArg\": dddArg,\n-\t\t\"echo\": echo,\n-\t\t\"makemap\": makemap,\n-\t\t\"oneArg\": oneArg,\n-\t\t\"typeOf\": typeOf,\n-\t\t\"vfunc\": vfunc,\n-\t\t\"zeroArgs\": zeroArgs,\n-\t\t\"stringer\": stringer,\
+\t\t\"add\": add,\n+\t\t\"count\": count,\n+\t\t\"dddArg\": dddArg,\n+\t\t\"echo\": echo,\n+\t\t\"makemap\": makemap,\n+\t\t\"mapOfThree\": mapOfThree,\n+\t\t\"oneArg\": oneArg,\n+\t\t\"stringer\": stringer,\n+\t\t\"typeOf\": typeOf,\n+\t\t\"vfunc\": vfunc,\n+\t\t\"zeroArgs\": zeroArgs,\
\t}\n \tfor _, test := range execTests {\
\t\tvar tmpl *Template\
コアとなるコードの解説
src/pkg/text/template/exec.go
の変更
evalArg
関数は、テンプレートの引数ノードを評価する役割を担っています。この関数内の switch
ステートメントは、異なる種類のノード(変数、パイプラインなど)を処理します。
追加された case *parse.IdentifierNode:
ブロックは、ノードが識別子(名前)である場合に実行されます。ここで、s.evalFunction(dot, arg, arg, nil, zero)
が呼び出されています。これは、この識別子が関数名であると解釈し、その関数を現在のコンテキスト (dot
) で評価することを意味します。関数の戻り値は reflect.Value
として返され、s.validateType
によって期待される型に検証されます。
この変更により、{{mapOfThree.three}}
のような式が評価される際、まず mapOfThree
が関数として認識され、その関数が実行されます。その結果(この場合は map[string]int{"three": 3}
)が次の評価ステップに渡され、.three
のフィールドアクセスが正しく行われるようになります。
src/pkg/text/template/exec_test.go
の変更
このファイルでは、バグを再現し、修正が正しく機能することを確認するための新しいテストケースが追加されています。
-
mapOfThree
関数の追加:func mapOfThree() interface{} { return map[string]int{"three": 3} }
この関数は、キーが
"three"
で値が3
のマップを返します。これは、テンプレート内で関数呼び出しの結果に対してフィールドアクセスを行うシナリオをシミュレートするために使用されます。 -
execTests
への新しいテストケースbug10
の追加:{"bug10", "{{mapOfThree.three}}-{{(mapOfThree).three}}", "3-3", 0, true},
"bug10"
: テストケースの名前。"{{mapOfThree.three}}-{{(mapOfThree).three}}"
: テスト対象のテンプレート文字列。{{mapOfThree.three}}
:mapOfThree
関数を呼び出し、その戻り値(マップ)から"three"
というキーの値を抽出します。{{(mapOfThree).three}}
: 明示的にmapOfThree
の結果を括弧で囲んでグループ化し、その結果に対して"three"
キーの値を抽出します。これは、括弧の有無にかかわらず同じ結果が得られることを確認するためです。
"3-3"
: 期待される出力。0
: テンプレートに渡されるデータ(このテストでは不要なため0
)。true
: テンプレートのパースが成功することを期待することを示します。
-
FuncMap
へのmapOfThree
の登録:testExecute
関数内で、mapOfThree
関数がFuncMap
に"mapOfThree"
という名前で登録されています。これにより、テンプレートからmapOfThree
という名前でこの関数を呼び出すことができるようになります。
この新しいテストケースは、修正前であれば失敗し、修正後には成功することで、バグが修正されたことを明確に示します。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/99645db9268cfe93f561ef2de013ea5f58304c79
- コミットメッセージ内の
golang.org/cl/7861046
は、このコミットの内容とは異なるGo CL(Change List)を指しているようです。Web検索の結果では、このCLはnet/http
パッケージのHTTP/2に関する変更を示しており、text/template
のバグ修正とは関連がありません。これは、コミットメッセージ内のCLリンクが誤っているか、または内部的なCL番号が変更された可能性を示唆しています。
参考にした情報源リンク
- Go言語
text/template
パッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語
reflect
パッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go言語のテンプレートに関するブログ記事やチュートリアル(一般的な知識として参照)
- Go言語のソースコード(
src/pkg/text/template/exec.go
およびsrc/pkg/text/template/exec_test.go
) - Web検索(
golang.org/cl/7861046
の確認のため)