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

[インデックス 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 型である場合、つまり関数型のフィールドである場合でも、引数を伴って呼び出すことができませんでした。

このコミットでは、以下のロジックが追加されています。

  1. フィールドがエクスポートされているかどうかのチェック: tField.PkgPath == "" は、フィールドがエクスポートされている(つまり、パッケージ外からアクセス可能である)ことを確認します。Goのテンプレートエンジンは、エクスポートされたフィールドとメソッドのみを評価できます。
  2. フィールドが関数型であるかどうかのチェック: field.Type().Kind() == reflect.Func という条件が追加されました。これにより、現在評価しているフィールドが関数型であるかどうかが判断されます。
  3. 関数フィールドの呼び出し: もしフィールドが関数型であれば、s.evalCall(dot, field, fieldName, args, final) が呼び出されます。evalCall は、Goのテンプレートエンジンがメソッドや関数を呼び出すための内部ヘルパー関数です。これにより、関数フィールドに引数を渡して実行できるようになります。
  4. エラーメッセージの改善: フィールドがメソッドでも関数でもないのに引数が渡された場合のエラーメッセージが、「%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)、fieldNameargs (テンプレートで渡された引数)、final (最終的な結果を格納する reflect.Value) を引数として取ります。これにより、テンプレート内で {{.Func "arg1" "arg2"}} のように関数フィールドを呼び出すことが可能になります。
  • 関数型でないフィールドに引数が渡された場合 (hasArgstrue) は、以前と同様にエラーを発生させますが、エラーメッセージが「メソッドでも関数でもない」というように、より正確な表現に修正されています。

この変更により、text/template は、構造体のフィールドが関数である場合でも、それを動的に呼び出す能力を獲得し、テンプレートの柔軟性と表現力が大幅に向上しました。

src/pkg/text/template/exec_test.go の変更

テストファイル exec_test.go には、この新機能の動作を検証するための新しいテストケースが追加されています。

  1. T 構造体への Func フィールドの追加:

    type T struct {
        // ...
        Func func(...string) string
        // ...
    }
    

    T 構造体に Func という名前の関数型のフィールドが追加されました。これは可変長引数 (...string) を取り、string を返す関数です。

  2. tVal 変数での Func フィールドの初期化:

    var tVal = &T{
        // ...
        Func:              func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") },
        // ...
    }
    

    tVal インスタンスで Func フィールドが具体的な関数として初期化されています。この関数は、渡された文字列スライスを + で結合し、<> で囲んだ文字列を返します。

  3. 新しいテストケースの追加:

    • {".Func", "-{{.Func}}-\", \"-<>-\", tVal, true}: 引数なしで Func フィールドを呼び出すテスト。期待される出力は "-<>-" です。
    • {".Func2", "-{{.Func he llo}}-\", \"-<he+llo>-\", tVal, true}: 2つの文字列引数 hello を渡して Func フィールドを呼び出すテスト。期待される出力は "-<he+llo>-" です。
    • {"pipeline func", "-{{.Func llo| .Funche }}-\", \"-<he+<llo>>-\", tVal, true}: パイプライン内で関数フィールドを呼び出すテスト。llo を引数として Func を呼び出した結果が、さらに he を引数として Func に渡されるという複雑なケースをテストしています。期待される出力は "-<he+<llo>>-" です。

これらのテストケースは、関数フィールドが引数なし、引数あり、そしてパイプライン内で正しく評価されることを確認しています。

関連リンク

参考にした情報源リンク