[インデックス 11928] ファイルの概要
このコミットは、Go言語の text/template
パッケージにおける重要な修正を導入しています。具体的には、構造体のフィールドが関数である場合に、テンプレートエンジンがその関数を正しく評価し、引数を渡して呼び出せるようにする変更です。これにより、テンプレート内でより柔軟なデータ操作が可能になります。
コミット
commit aca8071fd53fc4f60771fe816b1e7c20c5c674fb
Author: Rob Pike <r@golang.org>
Date: Wed Feb 15 16:05:34 2012 +1100
text/template: evaluate function fields
Just an oversight they didn't work and easy to address.
Fixes #3025.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5656059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aca8071fd53fc4f60771fe816b1e7c20c5c674fb
元コミット内容
text/template: evaluate function fields
Just an oversight they didn't work and easy to address.
Fixes #3025.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5656059
変更の背景
この変更は、Go言語の text/template
パッケージにおけるバグ修正(Issue #3025)に対応するものです。元々、text/template
パッケージは、テンプレート内で構造体のフィールドがメソッドである場合にはそのメソッドを呼び出す機能を持っていました。しかし、フィールドが単なる関数(func
型のフィールド)である場合には、引数を伴って呼び出すことができませんでした。これは、テンプレートエンジンがフィールドを評価する際に、それがメソッドであるか関数であるかの区別を適切に行っていなかったため、関数フィールドが引数を持つ場合にエラーとなるか、あるいは単に評価されないという問題を引き起こしていました。
この「見落とし (oversight)」は、テンプレートの柔軟性を制限し、開発者が構造体に関数型のフィールドを持たせてテンプレートから直接呼び出したい場合に不便をもたらしていました。このコミットは、この問題を解決し、text/template
がより直感的で強力なツールとして機能するようにするためのものです。
前提知識の解説
Go言語の text/template
パッケージ
text/template
パッケージは、Go言語に組み込まれているデータ駆動型テンプレートエンジンです。HTMLやテキストファイルを生成するために使用され、Goのデータ構造(構造体、マップ、スライスなど)をテンプレートにバインドして、動的なコンテンツを生成できます。
主な特徴は以下の通りです。
- データバインディング: テンプレートは、Goの任意のデータ構造を「ドット (
.
)」として参照できます。 - フィールドとメソッドの評価: テンプレート内で
{{.FieldName}}
や{{.MethodName}}
のように記述することで、現在のコンテキスト(ドット)のフィールドやメソッドにアクセスできます。メソッドの場合、引数を渡して呼び出すことも可能です。 - パイプライン:
|
演算子を使って、前のコマンドの出力を次のコマンドの入力として渡すことができます。 - 制御構造:
if
,range
,with
などの制御構造をサポートし、条件分岐や繰り返し処理を行えます。
Go言語の reflect
パッケージ
reflect
パッケージは、Go言語のランタイムリフレクション機能を提供します。これにより、プログラムは実行時に自身の構造を検査し、操作することができます。
reflect.Value
: Goの任意の型の値を表します。この型を通じて、値の型、フィールド、メソッドなどにアクセスできます。reflect.Type
: Goの任意の型の型情報を表します。Value.FieldByName(name string)
:reflect.Value
が構造体の場合、指定された名前のフィールドのreflect.Value
を返します。Type.FieldByName(name string)
:reflect.Type
が構造体の場合、指定された名前のフィールドのreflect.StructField
を返します。StructField.Index
: 構造体内のフィールドのインデックスのリストを返します。Value.FieldByIndex(index []int)
:reflect.Value
が構造体の場合、指定されたインデックスパスのフィールドのreflect.Value
を返します。Value.Type().Kind()
:reflect.Value
の基底型(struct
,func
,int
など)を返します。reflect.Func
:reflect.Kind
の一つで、値が関数であることを示します。
このコミットでは、reflect
パッケージを使用して、構造体のフィールドが関数型であるかどうかを動的に判断し、その関数を呼び出すためのロジックを追加しています。
技術的詳細
このコミットの核心は、src/pkg/text/template/exec.go
内の evalField
メソッドの変更にあります。evalField
メソッドは、テンプレート内で .
(ドット) の後に続くフィールド名が評価される際に呼び出されます。
変更前は、evalField
は構造体のフィールドを評価する際に、そのフィールドがメソッドであるかどうかを主にチェックしていました。フィールドがメソッドでない場合、引数が渡されるとエラーを発生させていました。しかし、フィールドが func
型である場合、つまり関数型のフィールドである場合でも、引数を伴って呼び出すことができませんでした。
このコミットでは、以下のロジックが追加されています。
- フィールドがエクスポートされているかどうかのチェック:
tField.PkgPath == ""
は、フィールドがエクスポートされている(つまり、パッケージ外からアクセス可能である)ことを確認します。Goのテンプレートエンジンは、エクスポートされたフィールドとメソッドのみを評価できます。 - フィールドが関数型であるかどうかのチェック:
field.Type().Kind() == reflect.Func
という条件が追加されました。これにより、現在評価しているフィールドが関数型であるかどうかが判断されます。 - 関数フィールドの呼び出し: もしフィールドが関数型であれば、
s.evalCall(dot, field, fieldName, args, final)
が呼び出されます。evalCall
は、Goのテンプレートエンジンがメソッドや関数を呼び出すための内部ヘルパー関数です。これにより、関数フィールドに引数を渡して実行できるようになります。 - エラーメッセージの改善: フィールドがメソッドでも関数でもないのに引数が渡された場合のエラーメッセージが、「
%s is not a method but has arguments
」から「%s is not a method or function but has arguments
」に修正され、より正確な情報を提供するようになりました。
これにより、テンプレートエンジンは、構造体のフィールドが関数である場合でも、それをメソッドと同様に引数を伴って呼び出すことができるようになり、テンプレートの表現力が向上しました。
コアとなるコードの変更箇所
src/pkg/text/template/exec.go
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -419,10 +419,14 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
tField, ok := receiver.Type().FieldByName(fieldName)
if ok {
field := receiver.FieldByIndex(tField.Index)
- if hasArgs {
- s.errorf("%s is not a method but has arguments", fieldName)
+ if tField.PkgPath == "" { // field is exported
+ // If it's a function, we must call it.
+ if field.Type().Kind() == reflect.Func {
+ return s.evalCall(dot, field, fieldName, args, final)
+ }
+ if hasArgs {
+ s.errorf("%s is not a method or function but has arguments", fieldName)
+ }
+ return field
}
- if tField.PkgPath == "" { // field is exported
- return field
- }
}
src/pkg/text/template/exec_test.go
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -59,6 +59,8 @@ type T struct {
PI *int
PSI *[]int
NIL *int
+ // Function (not method)
+ Func func(...string) string
// Template to test evaluation of templates.
Tmpl *Template
}
@@ -118,6 +118,7 @@ var tVal = &T{\
Err: errors.New("erroozle"),
PI: newInt(23),
PSI: newIntSlice(21, 22, 23),\
+ Func: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") },
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
}\
\
@@ -297,8 +300,13 @@ var execTests = []execTest{\
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
"true", tVal, true},\
\
+ // Function call
+ {".Func", "-{{.Func}}-\", \"-<>-\", tVal, true},\
+ {".Func2", "-{{.Func `he` `llo`}}-\", \"-<he+llo>-\", tVal, true},\
+\
// Pipelines.
{"pipeline", "-{{.Method0 | .Method2 .U16}}-\", \"-Method2: 16 M0-\", tVal, true},\
+ {"pipeline func", "-{{.Func `llo` | .Func `he` }}-\", \"-<he+<llo>>-\", tVal, true},\
\
// If.\
{"if true", "{{if true}}TRUE{{end}}", "TRUE", tVal, true},\
コアとなるコードの解説
src/pkg/text/template/exec.go
の変更
変更の中心は evalField
関数です。
元のコードでは、フィールドがエクスポートされている (tField.PkgPath == ""
) かつ引数がない (!hasArgs
) 場合にのみ、そのフィールドの値をそのまま返していました。引数がある (hasArgs
) 場合は、それがメソッドでない限りエラーを発生させていました。
修正後のコードでは、tField.PkgPath == ""
のチェックの内側に、さらに field.Type().Kind() == reflect.Func
という条件が追加されています。
- もしフィールドが関数型 (
reflect.Func
) であれば、s.evalCall
を呼び出してその関数を実行します。evalCall
は、dot
(現在のコンテキスト)、field
(関数を表すreflect.Value
)、fieldName
、args
(テンプレートで渡された引数)、final
(最終的な結果を格納するreflect.Value
) を引数として取ります。これにより、テンプレート内で{{.Func "arg1" "arg2"}}
のように関数フィールドを呼び出すことが可能になります。 - 関数型でないフィールドに引数が渡された場合 (
hasArgs
がtrue
) は、以前と同様にエラーを発生させますが、エラーメッセージが「メソッドでも関数でもない」というように、より正確な表現に修正されています。
この変更により、text/template
は、構造体のフィールドが関数である場合でも、それを動的に呼び出す能力を獲得し、テンプレートの柔軟性と表現力が大幅に向上しました。
src/pkg/text/template/exec_test.go
の変更
テストファイル exec_test.go
には、この新機能の動作を検証するための新しいテストケースが追加されています。
-
T
構造体へのFunc
フィールドの追加:type T struct { // ... Func func(...string) string // ... }
T
構造体にFunc
という名前の関数型のフィールドが追加されました。これは可変長引数 (...string
) を取り、string
を返す関数です。 -
tVal
変数でのFunc
フィールドの初期化:var tVal = &T{ // ... Func: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") }, // ... }
tVal
インスタンスでFunc
フィールドが具体的な関数として初期化されています。この関数は、渡された文字列スライスを+
で結合し、<
と>
で囲んだ文字列を返します。 -
新しいテストケースの追加:
{".Func", "-{{.Func}}-\", \"-<>-\", tVal, true}
: 引数なしでFunc
フィールドを呼び出すテスト。期待される出力は"-<>-"
です。{".Func2", "-{{.Func
he}}-\", \"-<he+llo>-\", tVal, true}
: 2つの文字列引数he
とllo
を渡してFunc
フィールドを呼び出すテスト。期待される出力は"-<he+llo>-"
です。{"pipeline func", "-{{.Func
llo| .Func
he}}-\", \"-<he+<llo>>-\", tVal, true}
: パイプライン内で関数フィールドを呼び出すテスト。llo
を引数としてFunc
を呼び出した結果が、さらにhe
を引数としてFunc
に渡されるという複雑なケースをテストしています。期待される出力は"-<he+<llo>>-"
です。
これらのテストケースは、関数フィールドが引数なし、引数あり、そしてパイプライン内で正しく評価されることを確認しています。
関連リンク
- Go Issue #3025: https://github.com/golang/go/issues/3025
- Go Change-Id: I2222222222222222222222222222222222222222 (これはコミットメッセージに記載されている
https://golang.org/cl/5656059
の Change-Id です) - Go
text/template
パッケージドキュメント: https://pkg.go.dev/text/template - Go
reflect
パッケージドキュメント: https://pkg.go.dev/reflect
参考にした情報源リンク
- Go Issue #3025:
text/template
: evaluate function fields - https://github.com/golang/go/issues/3025 - Go CL 5656059:
text/template
: evaluate function fields - https://go-review.googlesource.com/c/go/+/5656059 - Go
text/template
package documentation - https://pkg.go.dev/text/template - Go
reflect
package documentation - https://pkg.go.dev/reflect