[インデックス 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の確認のため)