[インデックス 12346] ファイルの概要
このコミットは、Go言語の text/template
パッケージにおける関数値の扱いに関する変更です。具体的には、以前導入された関数値の自動呼び出しが引き起こしていた曖昧さを解消し、明示的な call
ビルトイン関数を導入することで、より明確で意図的な関数呼び出しを可能にしています。これにより、テンプレートの挙動が予測しやすくなり、潜在的なバグのリスクが低減されます。
コミット
commit f1d3ff1660f227154d05297654dc58c052711c34
Author: Rob Pike <r@golang.org>
Date: Sat Mar 3 23:14:20 2012 +1100
text/template: clean up function values
The recent addition of automatic function invocation generated
some troublesome ambiguities. Restore the previous behavior
and compensate by providing a "call" builtin to make it easy to
do what the automatic invocation did, but in a clear and explicit
manner.
Fixes #3140.
At least for now.
R=golang-dev, dsymonds, r
CC=golang-dev
https://golang.org/cl/5720065
---
src/pkg/text/template/doc.go | 23 +++++++++++-------\n src/pkg/text/template/exec.go | 5 +---\n src/pkg/text/template/exec_test.go | 28 +++++++++++++++++-----\n src/pkg/text/template/funcs.go | 48 ++++++++++++++++++++++++++++++++++++++\n 4 files changed, 86 insertions(+), 18 deletions(-)
diff --git a/src/pkg/text/template/doc.go b/src/pkg/text/template/doc.go
index ae91f4a541..10e0f7fc37 100644
--- a/src/pkg/text/template/doc.go
+++ b/src/pkg/text/template/doc.go
@@ -142,11 +142,6 @@ An argument is a simple value, denoted by one of the following.\n .Field1.Key1.Method1.Field2.Key2.Method2
Methods can also be evaluated on variables, including chaining:\n $x.Method1.Field
-\t- The name of a niladic function-valued struct field of the data,\n-\t preceded by a period, such as\n-\t\t.Function
-\t Function-valued fields behave like methods (of structs) but do not\n-\t pass a receiver.\n \t- The name of a niladic function, such as\n \t\tfun
\t The result is the value of invoking the function, fun(). The return\n@@ -155,6 +150,10 @@ An argument is a simple value, denoted by one of the following.\n \n Arguments may evaluate to any type; if they are pointers the implementation\n automatically indirects to the base type when required.\n+If an evaluation yields a function value, such as a function-valued\n+field of a struct, the function is not invoked automatically, but it\n+can be used as a truth value for an if action and the like. To invoke\n+it, use the call function, defined below.\n \n A pipeline is a possibly chained sequence of \"commands\". A command is a simple\n value (argument) or a function or method call, possibly with multiple arguments:\n@@ -167,9 +166,6 @@ value (argument) or a function or method call, possibly with multiple arguments:\n \t\tThe result is the value of calling the method with the\n \t\targuments:\n \t\t\tdot.Method(Argument1, etc.)\n-\t.Function [Argument...]\n-\t\tA function-valued field of a struct works like a method but does\n-\t\tnot pass the receiver.\n \tfunctionName [Argument...]\n \t\tThe result is the value of calling the function associated\n \t\twith the name:\n@@ -257,6 +253,17 @@ Predefined global functions are named as follows.\n \t\tfirst empty argument or the last argument, that is,\n \t\t\"and x y\" behaves as \"if x then y else x\". All the\n \t\targuments are evaluated.\n+\tcall\n+\t\tReturns the result of calling the first argument, which\n+\t\tmust be a function, with the remaining arguments as parameters.\n+\t\tThus \"call .X.Y 1 2\" is, in Go notation, dot.X.Y(1, 2) where\n+\t\tY is a func-valued field, map entry, or the like.\n+\t\tThe first argument must be the result of an evaluation\n+\t\tthat yields a value of function type (as distinct from\n+\t\ta predefined function such as print). The function must\n+\t\treturn either one or two result values, the second of which\n+\t\tis of type error. If the arguments don\'t match the function\n+\t\tor the returned error value is non-nil, execution stops.\n \thtml\n \t\tReturns the escaped HTML equivalent of the textual\n \t\trepresentation of its arguments.\ndiff --git a/src/pkg/text/template/exec.go b/src/pkg/text/template/exec.go
index af745286c0..ad0118e4c6 100644
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -421,11 +421,8 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
field := receiver.FieldByIndex(tField.Index)
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)
+ s.errorf("%s has arguments but cannot be invoked as function", fieldName)
}
return field
}
diff --git a/src/pkg/text/template/exec_test.go b/src/pkg/text/template/exec_test.go
index 159cf5100d..83ca0022ba 100644
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -60,7 +60,9 @@ type T struct {
PSI *[]int
NIL *int
// Function (not method)
- Func func(...string) string
+ BinaryFunc func(string, string) string
+ VariadicFunc func(...string) string
+ VariadicFuncInt func(int, ...string) string
// Template to test evaluation of templates.
Tmpl *Template
}
@@ -120,7 +122,9 @@ var tVal = &T{
Err: errors.New("erroozle"),
PI: newInt(23),
PSI: newIntSlice(21, 22, 23),\n-\tFunc: func(s ...string) string { return fmt.Sprint(\"<\", strings.Join(s, \"+\"), \">\") },
+\tBinaryFunc: func(a, b string) string { return fmt.Sprintf(\"[%s=%s]\", a, b) },
+\tVariadicFunc: func(s ...string) string { return fmt.Sprint(\"<\", strings.Join(s, \"+\"), \">\") },
+\tVariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, \"=<\", strings.Join(s, \"+\"), \">\") },
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
}
@@ -300,13 +304,25 @@ var execTests = []execTest{
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
"true", tVal, true},
-\t// Function call
-\t{\".Func\", \"-{{.Func}}-\", \"-<>-\", tVal, true},\n-\t{\".Func2\", \"-{{.Func `he` `llo`}}-\", \"-<he+llo>-\", tVal, true},\n+\t// Function call builtin.
+\t{\".BinaryFunc\", "{{call .BinaryFunc `1` `2`}}", "[1=2]", tVal, true},\n+\t{\".VariadicFunc0\", "{{call .VariadicFunc}}", "<>", tVal, true},\n+\t{\".VariadicFunc2\", "{{call .VariadicFunc `he` `llo`}}", "<he+llo>", tVal, true},\n+\t{\".VariadicFuncInt\", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=<he+llo>", tVal, true},\n+\t{\"if .BinaryFunc call\", "{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}", "[1=2]", tVal, true},\n+\t{\"if not .BinaryFunc call\", "{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}", "No", tVal, true},\n+\n+\t// Erroneous function calls (check args).
+\t{\".BinaryFuncTooFew\", "{{call .BinaryFunc `1`}}", "", tVal, false},\n+\t{\".BinaryFuncTooMany\", "{{call .BinaryFunc `1` `2` `3`}}", "", tVal, false},\n+\t{\".BinaryFuncBad0\", "{{call .BinaryFunc 1 3}}", "", tVal, false},\n+\t{\".BinaryFuncBad1\", "{{call .BinaryFunc `1` 3}}", "", tVal, false},\n+\t{\".VariadicFuncBad0\", "{{call .VariadicFunc 3}}", "", tVal, false},\n+\t{\".VariadicFuncIntBad0\", "{{call .VariadicFuncInt `x`}}", "", tVal, false},\n \n \t// Pipelines.
\t{\"pipeline\", \"-{{.Method0 | .Method2 .U16}}-\", \"-Method2: 16 M0-\", tVal, true},\n-\t{\"pipeline func\", \"-{{.Func `llo` | .Func `he` }}-\", \"-<he+<llo>>-\", tVal, true},\n+\t{\"pipeline func\", \"-{{call .VariadicFunc `llo` | call .VariadicFunc `he` }}-\", \"-<he+<llo>>-\", tVal, true},\n \n \t// If.
\t{\"if true\", \"{{if true}}TRUE{{end}}\", \"TRUE\", tVal, true},\ndiff --git a/src/pkg/text/template/funcs.go b/src/pkg/text/template/funcs.go
index d6e4bf1a21..525179cb49 100644
--- a/src/pkg/text/template/funcs.go
+++ b/src/pkg/text/template/funcs.go
@@ -24,6 +24,7 @@ type FuncMap map[string]interface{}\n
var builtins = FuncMap{
"and": and,
+\t"call": call,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
@@ -151,6 +152,53 @@ func length(item interface{}) (int, error) {
return 0, fmt.Errorf("len of type %s", v.Type())
}
+// Function invocation
+
+// call returns the result of evaluating the the first argument as a function.
+// The function must return 1 result, or 2 results, the second of which is an error.
+func call(fn interface{}, args ...interface{}) (interface{}, error) {
+ v := reflect.ValueOf(fn)
+ typ := v.Type()
+ if typ.Kind() != reflect.Func {
+ return nil, fmt.Errorf("non-function of type %s", typ)
+ }
+ if !goodFunc(typ) {
+ return nil, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
+ }
+ numIn := typ.NumIn()
+ var dddType reflect.Type
+ if typ.IsVariadic() {
+ if len(args) < numIn-1 {
+ return nil, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
+ }
+ dddType = typ.In(numIn - 1).Elem()
+ } else {
+ if len(args) != numIn {
+ return nil, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
+ }
+ }
+ argv := make([]reflect.Value, len(args))
+ for i, arg := range args {
+ value := reflect.ValueOf(arg)
+ // Compute the expected type. Clumsy because of variadics.
+ var argType reflect.Type
+ if !typ.IsVariadic() || i < numIn-1 {
+ argType = typ.In(i)
+ } else {
+ argType = dddType
+ }
+ if !value.Type().AssignableTo(argType) {
+ return nil, fmt.Errorf("arg %d has type %s; should be %s", i, value.Type(), argType)
+ }
+ argv[i] = reflect.ValueOf(arg)
+ }
+ result := v.Call(argv)
+ if len(result) == 2 {
+ return result[0].Interface(), result[1].Interface().(error)
+ }
+ return result[0].Interface(), nil
+}
+
// Boolean logic.
func truth(a interface{}) bool {
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f1d3ff1660f227154d05297654dc58c052711c34
元コミット内容
text/template: clean up function values
The recent addition of automatic function invocation generated
some troublesome ambiguities. Restore the previous behavior
and compensate by providing a "call" builtin to make it easy to
do what the automatic invocation did, but in a clear and explicit
manner.
Fixes #3140.
At least for now.
変更の背景
このコミットの背景には、Go言語の text/template
パッケージにおける「関数値の自動呼び出し」という機能が引き起こした問題があります。
元々、text/template
では、テンプレート内で構造体のフィールドが関数型である場合、そのフィールドを参照するだけで自動的に関数が呼び出される挙動がありました。例えば、{{.MyFunc}}
のように記述すると、MyFunc
が関数であれば自動的に実行され、その結果がテンプレートに埋め込まれるというものです。
しかし、この「自動呼び出し」は、特に引数を取らない関数(niladic function)の場合に、意図しない挙動や曖昧さを生じさせることが判明しました。例えば、あるフィールドが関数値であると同時に、その関数が引数を取らない場合、テンプレートの記述によっては、そのフィールドが単なる値として評価されるべきか、それとも関数として呼び出されるべきか、システムが判断に迷うケースが発生しました。これにより、テンプレートの挙動が予測不能になったり、デバッグが困難になったりする「厄介な曖昧さ (troublesome ambiguities)」が生じていました。
この問題は、GoのIssue #3140で報告され、議論されていました。このコミットは、その問題への対応として、自動呼び出しの挙動を元に戻し、代わりに明示的な call
ビルトイン関数を提供することで、開発者が意図的に関数を呼び出せるようにすることを目的としています。これにより、テンプレートの記述がより明確になり、曖昧さが排除され、予測可能な挙動が保証されるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語および text/template
パッケージに関する知識が必要です。
1. Go言語の text/template
パッケージ
text/template
パッケージは、Go言語でテキストベースのテンプレートを生成するための強力なツールです。HTML、XML、プレーンテキストなど、様々な形式の出力を生成するのに使用されます。主な特徴は以下の通りです。
- データ駆動: テンプレートは、Goの構造体、マップ、スライスなどのデータ構造を「コンテキスト」として受け取り、そのデータに基づいて出力を生成します。
- アクション: テンプレート内では、
{{...}}
で囲まれた「アクション」と呼ばれる特殊な構文を使用して、データの参照、条件分岐 (if
)、繰り返し (range
)、関数呼び出しなどを行います。 - パイプライン: アクションはパイプライン形式で記述でき、前のコマンドの出力が次のコマンドの入力として渡されます。例:
{{.Name | upper}}
- 関数: テンプレート内で使用できる関数は、Goの関数を登録することで拡張できます。これらの関数は、テンプレートのデータや他の値を引数として受け取り、結果を返します。
2. Go言語における関数値 (Function Values)
Go言語では、関数は第一級オブジェクト(first-class citizen)です。これは、関数を変数に代入したり、関数の引数として渡したり、関数の戻り値として返したりできることを意味します。このような関数を「関数値」と呼びます。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
// 関数を変数に代入
myFunc := add
fmt.Println(myFunc(1, 2)) // 出力: 3
// 関数を引数として渡す
process := func(f func(int, int) int, x, y int) int {
return f(x, y)
}
fmt.Println(process(add, 3, 4)) // 出力: 7
}
text/template
の文脈では、テンプレートに渡されるデータ構造のフィールドが関数型である場合、それが「関数値」として扱われます。
3. Go言語の reflect
パッケージ
reflect
パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)したり、変更したりするための機能を提供します。このコミットで追加される call
関数は、この reflect
パッケージを多用しています。
reflect.ValueOf(interface{})
: 任意のGoの値をreflect.Value
型に変換します。これにより、その値の実行時情報を取得できます。reflect.Type()
:reflect.Value
からその値の型情報(reflect.Type
)を取得します。reflect.Kind()
:reflect.Type
からその型の種類(reflect.Func
,reflect.Struct
,reflect.Int
など)を取得します。reflect.Value.Call([]reflect.Value)
:reflect.Value
が関数を表す場合、このメソッドを使ってその関数を呼び出すことができます。引数は[]reflect.Value
のスライスとして渡され、戻り値も[]reflect.Value
のスライスとして返されます。reflect.Type.NumIn()
: 関数の引数の数を返します。reflect.Type.NumOut()
: 関数の戻り値の数を返します。reflect.Type.In(i int)
:i
番目の引数の型を返します。reflect.Type.Out(i int)
:i
番目の戻り値の型を返します。reflect.Type.IsVariadic()
: 関数が可変長引数(variadic arguments)を取るかどうかを返します。reflect.Type.Elem()
: ポインタ型、配列型、スライス型、マップ型、チャネル型の場合、その要素の型を返します。可変長引数の場合、最後の引数(...T
)の要素型(T
)を取得するために使用されます。reflect.Value.AssignableTo(Type)
: あるreflect.Value
の型が、指定されたreflect.Type
に代入可能かどうかをチェックします。
これらのリフレクション機能は、call
関数が任意の関数型を動的に呼び出し、引数の型チェックや戻り値の処理を行うために不可欠です。
4. 可変長引数 (Variadic Arguments)
Go言語では、関数の最後の引数に ...
を付けることで、その関数が任意の数の引数を受け取れるようにすることができます。これを可変長引数と呼びます。
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 出力: 6
fmt.Println(sum(10, 20)) // 出力: 30
fmt.Println(sum()) // 出力: 0
}
call
関数は、可変長引数を取る関数も正しく処理できるように設計されています。
技術的詳細
このコミットの技術的な変更は、主に以下の3つの側面から構成されます。
1. 関数値の自動呼び出しの廃止 (src/pkg/text/template/exec.go
および src/pkg/text/template/doc.go
)
以前の text/template
の実装では、テンプレートの評価中に、データコンテキスト内のフィールドが関数型である場合、そのフィールドが自動的に呼び出されていました。これは、src/pkg/text/template/exec.go
の evalField
関数内で、field.Type().Kind() == reflect.Func
の条件が真の場合に s.evalCall
を呼び出すロジックによって実現されていました。
このコミットでは、exec.go
から以下のコードが削除されました。
- if field.Type().Kind() == reflect.Func {
- return s.evalCall(dot, field, fieldName, args, final)
- }
これにより、フィールドが関数値であっても、自動的に呼び出されることはなくなりました。代わりに、その関数値はそのままの値として扱われます。もし、その関数値に対して引数が与えられた場合(つまり、自動呼び出しを期待するような記述があった場合)、エラーを発生させるように変更されています。
- s.errorf("%s is not a method or function but has arguments", fieldName)
+ s.errorf("%s has arguments but cannot be invoked as function", fieldName)
また、src/pkg/text/template/doc.go
のドキュメントも更新され、関数値のフィールドが自動的に呼び出されないこと、そして call
関数を使って明示的に呼び出す必要があることが明記されました。
2. call
ビルトイン関数の導入 (src/pkg/text/template/funcs.go
および src/pkg/text/template/doc.go
)
自動呼び出しの廃止に伴い、テンプレート内で関数を明示的に呼び出すための新しいビルトイン関数 call
が導入されました。この関数は src/pkg/text/template/funcs.go
に追加され、builtins
マップに登録されています。
call
関数のシグネチャは func call(fn interface{}, args ...interface{}) (interface{}, error)
です。
この関数は、第一引数として呼び出したい関数値を受け取り、残りの引数をその関数に渡します。
call
関数の実装は、Goのリフレクション機能 (reflect
パッケージ) を駆使して、動的に関数を呼び出します。主な処理は以下の通りです。
- 関数値の検証:
reflect.ValueOf(fn)
で関数値をreflect.Value
に変換し、typ.Kind() != reflect.Func
で関数型であることを確認します。関数でなければエラーを返します。 - 戻り値の検証:
goodFunc(typ)
ヘルパー関数(このコミットでは追加されていないが、既存の関数)を使って、関数の戻り値が1つまたは2つ(2つ目の戻り値がerror
型の場合)であることを確認します。これは、テンプレートエンジンが処理できる戻り値の形式に限定するためです。 - 引数の数と型の検証:
- 呼び出される関数の引数の数 (
numIn
) と、call
関数に渡された引数の数 (len(args)
) を比較し、数が一致しない場合はエラーを返します。 - 可変長引数 (
typ.IsVariadic()
) の場合も適切に処理されます。可変長引数の場合は、必須引数の数と可変長引数の要素型を考慮して引数の数を検証します。 - 各引数について、
reflect.ValueOf(arg)
でreflect.Value
に変換し、value.Type().AssignableTo(argType)
を使って、呼び出される関数の期待する引数型に代入可能かどうかをチェックします。型が一致しない場合はエラーを返します。
- 呼び出される関数の引数の数 (
- 関数の呼び出し: 準備された引数 (
argv
) を使って、v.Call(argv)
を呼び出し、実際の関数を実行します。 - 戻り値の処理: 関数の戻り値 (
result
) を受け取り、戻り値が2つの場合は2つ目がerror
型であることを確認し、エラーがあればそれを返します。そうでなければ、最初の戻り値を返します。
src/pkg/text/template/doc.go
にも call
関数の使用方法と挙動に関する詳細な説明が追加されています。
3. テストケースの更新 (src/pkg/text/template/exec_test.go
)
変更された挙動と新しく追加された call
関数を検証するために、テストケースが大幅に更新されています。
T
構造体にBinaryFunc
,VariadicFunc
,VariadicFuncInt
といった新しい関数フィールドが追加され、それぞれ異なる引数シグネチャを持つ関数値が割り当てられています。- 以前の自動呼び出しに依存していたテストケースは削除または修正され、代わりに
call
関数を使った明示的な関数呼び出しのテストが追加されています。 call
関数の正しい使用方法(引数の数、型の一致)と、誤った使用方法(引数の不足、過多、型不一致)に対するエラーハンドリングを検証するテストケースが多数追加されています。これにより、call
関数の堅牢性が保証されます。- パイプライン内での
call
関数の使用例も追加され、その互換性が確認されています。
これらの変更により、text/template
パッケージは、関数値の扱いにおいてより明確で予測可能な挙動を提供するようになり、開発者は意図的に関数を呼び出すことができるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の2ファイルです。
-
src/pkg/text/template/exec.go
:evalField
関数内で、構造体のフィールドが関数型である場合の自動呼び出しロジックが削除されました。- 具体的には、
if field.Type().Kind() == reflect.Func { return s.evalCall(...) }
のブロックが削除されています。 - これにより、フィールドが関数値であっても、明示的に呼び出されない限り実行されなくなりました。
-
src/pkg/text/template/funcs.go
:- 新しいビルトイン関数
call
が追加されました。 builtins
マップに"call": call,
が追加され、テンプレートからcall
関数が利用可能になりました。call
関数の実装が追加され、リフレクション (reflect
パッケージ) を使用して、任意の関数値とその引数を動的に処理し、呼び出すロジックが記述されています。
- 新しいビルトイン関数
これらの変更が、text/template
における関数値の評価挙動の根本的な変更を構成しています。
コアとなるコードの解説
src/pkg/text/template/exec.go
の変更
func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node) (reflect.Value, error) {
// ... (既存のコード) ...
// フィールドがエクスポートされている場合
if tField.PkgPath == "" {
field := receiver.FieldByIndex(tField.Index)
// 以前はここに、fieldが関数型であれば自動的に呼び出すロジックがあったが、削除された。
// if field.Type().Kind() == reflect.Func {
// return s.evalCall(dot, field, fieldName, args, final)
// }
if hasArgs {
// 引数があるにもかかわらず、関数として呼び出せない場合にエラーを発生させる
s.errorf("%s has arguments but cannot be invoked as function", fieldName)
}
return field // 関数値であっても、ここでは値としてそのまま返される
}
// ... (既存のコード) ...
}
この変更により、text/template
は、構造体のフィールドが関数値であっても、それを自動的に呼び出すことをやめました。代わりに、その関数値は単なる値として扱われます。もしテンプレート内でその関数値に対して引数が与えられた場合(例: {{.MyFunc "arg"}}
)、それはもはや自動呼び出しとは見なされず、エラーとなります。これは、テンプレートの挙動をより明示的で予測可能なものにするための重要な変更です。
src/pkg/text/template/funcs.go
の call
関数
// call returns the result of evaluating the the first argument as a function.
// The function must return 1 result, or 2 results, the second of which is an error.
func call(fn interface{}, args ...interface{}) (interface{}, error) {
v := reflect.ValueOf(fn) // 呼び出す関数値をreflect.Valueに変換
typ := v.Type() // 関数の型情報を取得
// 第一引数が関数型であることを確認
if typ.Kind() != reflect.Func {
return nil, fmt.Errorf("non-function of type %s", typ)
}
// 関数の戻り値の数を検証 (1つ、または2つで2つ目がerror型)
// goodFuncは既存のヘルパー関数で、このコミットでは変更されていない
if !goodFunc(typ) {
return nil, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
}
numIn := typ.NumIn() // 関数の期待する引数の数
var dddType reflect.Type // 可変長引数の要素型を保持するための変数
// 可変長引数を持つ関数かどうかのチェック
if typ.IsVariadic() {
// 可変長引数関数の場合、必須引数の数より少ない引数ではエラー
if len(args) < numIn-1 {
return nil, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
}
// 可変長引数の要素型を取得 (例: ...string の場合、string型)
dddType = typ.In(numIn - 1).Elem()
} else {
// 可変長引数でない場合、引数の数が厳密に一致しないとエラー
if len(args) != numIn {
return nil, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
}
}
// 呼び出す関数に渡す引数をreflect.Valueのスライスとして準備
argv := make([]reflect.Value, len(args))
for i, arg := range args {
value := reflect.ValueOf(arg) // 渡された引数をreflect.Valueに変換
// 期待される引数の型を計算 (可変長引数の場合を考慮)
var argType reflect.Type
if !typ.IsVariadic() || i < numIn-1 {
// 可変長引数でない場合、または可変長引数だが必須引数の場合
argType = typ.In(i)
} else {
// 可変長引数の場合、可変長引数の要素型を使用
argType = dddType
}
// 引数の型が期待される型に代入可能かチェック
if !value.Type().AssignableTo(argType) {
return nil, fmt.Errorf("arg %d has type %s; should be %s", i, value.Type(), argType)
}
argv[i] = reflect.ValueOf(arg) // 変換した引数をスライスに追加
}
// 準備した引数で関数を呼び出す
result := v.Call(argv)
// 関数の戻り値を処理
if len(result) == 2 {
// 戻り値が2つの場合、2つ目がerror型であることを期待
return result[0].Interface(), result[1].Interface().(error)
}
// 戻り値が1つの場合
return result[0].Interface(), nil
}
call
関数は、text/template
内でGoの関数を動的に呼び出すための汎用的なメカニズムを提供します。リフレクションを深く利用することで、引数の数や型、可変長引数の有無、戻り値の形式など、Go関数の多様なシグネチャに対応しています。これにより、テンプレートの柔軟性が向上しつつも、明示的な呼び出しによって挙動の予測可能性が保たれます。
関連リンク
- Go Issue #3140: https://github.com/golang/go/issues/3140
- このコミットが修正した問題に関する議論と背景情報が記載されています。
- Go CL 5720065: https://golang.org/cl/5720065
- このコミットのGo Code Reviewツールにおける変更セットのリンクです。
参考にした情報源リンク
- Go言語の
text/template
パッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語の
reflect
パッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go言語の可変長引数に関する情報 (Go公式ブログなど): https://go.dev/blog/go-slices-usage-and-internals (スライスと関連して可変長引数も説明されることが多い)
- Go言語における関数値に関する情報: https://go.dev/tour/moretypes/25 (Go Tourの関数値に関するセクション)
- Go言語の
fmt.Errorf
に関する情報: https://pkg.go.dev/fmt#Errorf
これらの公式ドキュメントやGo言語の基本的な概念に関する情報源は、このコミットの変更内容と背景を深く理解するために参照されました。