[インデックス 17171] ファイルの概要
このコミットは、Go言語の text/template
パッケージにおける call
ビルトイン関数が、関数が nil
エラーを返した場合に正しく動作しない問題を修正します。具体的には、2つの戻り値を持つ関数が (value, nil)
を返した際に、call
ビルトインが nil
をエラー型に変換しようとしてパニックを起こす問題を解決します。この修正により、nil
エラーが正しく処理されるようになり、関連するテストも追加されています。
コミット
- コミットハッシュ: 83348a13fb40ac80e2587e27c29d18360177f3b1
- Author: Elias Naur elias.naur@gmail.com
- Date: Tue Aug 13 11:11:05 2013 +1000
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/83348a13fb40ac80e2587e27c29d18360177f3b1
元コミット内容
text/template: Make function call builtin handle nil errors correctly
The call builtin unconditionally tries to convert a second return value from a function to the error type. This fails in case nil is returned, effectively making call useless for functions returning two values.
This CL adds a nil check for the second return value, and adds a test.
Note that for regular function and method calls the nil error case is handled correctly and is verified by a test.
R=r
CC=golang-dev
https://golang.org/cl/12804043
変更の背景
Go言語の text/template
パッケージは、テキストベースのテンプレートを生成するための強力なツールです。テンプレート内でGoの関数を呼び出す機能(call
ビルトイン)は非常に便利ですが、特定のシナリオで問題が発生していました。
問題は、テンプレートから呼び出されるGoの関数が2つの戻り値(例: (string, error)
)を持ち、かつエラー値として nil
を返した場合に発生しました。text/template
の call
ビルトインは、関数の2番目の戻り値がエラー型であると仮定し、無条件にその値を error
インターフェースに型アサートしようとします。しかし、Goにおいて nil
は特定の型を持たないため、nil
を error
インターフェースに型アサートしようとすると、ランタイムパニック(interface conversion: interface {} is nil, not error
)が発生していました。
この挙動は、エラーがない場合に nil
を返すというGoの一般的なエラーハンドリングパターンと矛盾し、call
ビルトインが2つの戻り値を持つ関数(特にエラーを返す可能性のある関数)に対して実質的に使用不可能にしていました。このコミットは、この問題を解決し、call
ビルトインが nil
エラーを正しく処理できるようにすることを目的としています。
前提知識の解説
text/template
パッケージ
text/template
パッケージは、Go言語でテキストベースの出力を生成するためのテンプレートエンジンを提供します。HTML、XML、プレーンテキストなど、様々な形式のドキュメントを動的に生成するのに使用されます。テンプレートは、プレースホルダーや制御構造(if
, range
, with
など)を含むテキストと、Goのデータ構造を組み合わせて使用します。
call
ビルトイン関数
text/template
パッケージには、テンプレート内でGoの関数やメソッドを呼び出すための call
ビルトイン関数が用意されています。これにより、テンプレート内で複雑なロジックを実行したり、外部のヘルパー関数を利用したりすることが可能になります。
例:
func MyFunc(a, b int) int {
return a + b
}
tmpl, _ := template.New("test").Parse("Result: {{call .MyFunc 1 2}}")
data := map[string]interface{}{"MyFunc": MyFunc}
tmpl.Execute(os.Stdout, data) // Output: Result: 3
Goのエラーハンドリング
Go言語では、エラーは通常、関数の最後の戻り値として error
インターフェース型で返されます。エラーがない場合は nil
が返されます。これはGoのイディオムであり、多くの標準ライブラリやユーザー定義関数で採用されています。
func doSomething() (string, error) {
// エラーが発生しない場合
return "success", nil
// エラーが発生した場合
// return "", errors.New("something went wrong")
}
reflect
パッケージと reflect.Value.IsNil()
reflect
パッケージは、Goのプログラムが実行時に自身の構造を検査・操作するための機能を提供します。text/template
パッケージは、テンプレートからGoの関数を呼び出す際に、この reflect
パッケージを内部的に使用して、関数の引数を処理し、戻り値を取得します。
reflect.Value
は、Goの任意の型の値を表す構造体です。reflect.Value
には、その値が nil
であるかどうかをチェックするための IsNil()
メソッドがあります。このメソッドは、ポインタ、インターフェース、マップ、スライス、チャネル、関数などの参照型に対してのみ有効です。これらの型が nil
である場合に true
を返します。
技術的詳細
このコミットの核心は、text/template
パッケージの funcs.go
ファイルにある call
関数(テンプレートの call
ビルトインの実装)の修正です。
修正前の call
関数は、呼び出されたGoの関数が2つの戻り値を持つ場合、以下のようなロジックで処理していました。
// 修正前 (簡略化)
if len(result) == 2 {
// 2番目の戻り値を無条件に error 型にアサート
return result[0].Interface(), result[1].Interface().(error)
}
ここで result
は []reflect.Value
型であり、result[1]
はGoの関数の2番目の戻り値を表す reflect.Value
です。問題は、Goの関数が (string, nil)
のように nil
をエラーとして返した場合、result[1]
は reflect.Value
として nil
をラップしていますが、その Interface()
メソッドが返す値はGoの nil
です。このGoの nil
を .(error)
で error
インターフェースに型アサートしようとすると、Goのランタイムは「nil
は error
インターフェースではない」と判断し、パニックを引き起こします。
このコミットでは、この問題に対処するために、2番目の戻り値が nil
であるかどうかを reflect.Value.IsNil()
メソッドを使って明示的にチェックする条件を追加しました。
// 修正後
if len(result) == 2 && !result[1].IsNil() {
return result[0].Interface(), result[1].Interface().(error)
}
この変更により、2番目の戻り値が nil
の場合は !result[1].IsNil()
が false
となり、if
ブロック内の result[1].Interface().(error)
という危険な型アサートがスキップされます。代わりに、if
ブロックの外の return result[0].Interface(), nil
が実行され、テンプレート側にはエラーがないことを示す nil
が正しく渡されます。
また、この修正を検証するために、exec_test.go
に新しいテストケースが追加されました。このテストケースは、ErrFunc
という (string, error)
を返し、常に nil
エラーを返す関数を定義し、{{call .ErrFunc}}
が正しく "bla" を返すことを確認します。
コアとなるコードの変更箇所
src/pkg/text/template/exec_test.go
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -64,6 +64,7 @@ type T struct {
VariadicFunc func(...string) string
VariadicFuncInt func(int, ...string) string
NilOKFunc func(*int) bool
+ ErrFunc func() (string, error)
// Template to test evaluation of templates.
Tmpl *Template
// Unexported field; cannot be accessed by template.
@@ -129,6 +130,7 @@ var tVal = &T{
VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") },
VariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, "=<", strings.Join(s, "+"), ">") },
NilOKFunc: func(s *int) bool { return s == nil },
+ ErrFunc: func() (string, error) { return "bla", nil },
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
}
@@ -322,6 +324,7 @@ var execTests = []execTest{
{"if .BinaryFunc call", "{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}", "[1=2]", tVal, true},
{"if not .BinaryFunc call", "{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}", "No", tVal, true},
{"Interface Call", `{{stringer .S}}`, "foozle", map[string]interface{}{"S": bytes.NewBufferString("foozle")}, true},
+ {".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
// Erroneous function calls (check args).
{".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false},
src/pkg/text/template/funcs.go
--- a/src/pkg/text/template/funcs.go
+++ b/src/pkg/text/template/funcs.go
@@ -199,7 +199,7 @@ func call(fn interface{}, args ...interface{}) (interface{}, error) {
argv[i] = value
}\n result := v.Call(argv)
-\tif len(result) == 2 {\n+\tif len(result) == 2 && !result[1].IsNil() {\n \t\treturn result[0].Interface(), result[1].Interface().(error)\n \t}\n \treturn result[0].Interface(), nil
コアとなるコードの解説
src/pkg/text/template/funcs.go
の変更
このファイルには、text/template
パッケージのビルトイン関数が実装されています。call
関数は、テンプレート内で {{call ...}}
として使用されるビルトインの実際のロジックを含んでいます。
変更された行:
- if len(result) == 2 {
+ if len(result) == 2 && !result[1].IsNil() {
result
は、reflect.Value.Call()
メソッドによって返される[]reflect.Value
型のスライスです。これは、呼び出されたGoの関数の戻り値をreflect.Value
のスライスとして保持します。len(result) == 2
は、呼び出されたGoの関数が2つの戻り値を持つかどうかをチェックします。!result[1].IsNil()
が追加された新しい条件です。result[1]
は、Goの関数の2番目の戻り値を表すreflect.Value
です。IsNil()
メソッドは、そのreflect.Value
が表す値がnil
であるかどうかをチェックします。- この条件が追加されたことで、2番目の戻り値が
nil
でない場合にのみ、その値をerror
インターフェースに型アサートする処理(result[1].Interface().(error)
)が実行されるようになります。 - もし
result[1]
がnil
であれば、!result[1].IsNil()
はfalse
となり、if
ブロック全体がスキップされます。その結果、call
関数はreturn result[0].Interface(), nil
を実行し、テンプレート側にはエラーがないことを正しく伝えます。
この変更により、nil
エラーが error
インターフェースに不適切に型アサートされることによるランタイムパニックが回避されます。
src/pkg/text/template/exec_test.go
の変更
このファイルには、text/template
パッケージの実行に関するテストが含まれています。
新しいフィールド ErrFunc
が T
構造体に追加されました:
type T struct {
// ...
ErrFunc func() (string, error)
// ...
}
これは、テストで使用する構造体 T
に、string
と error
を返す関数を保持するためのフィールドを追加します。
tVal
変数に ErrFunc
の実装が追加されました:
var tVal = &T{
// ...
ErrFunc: func() (string, error) { return "bla", nil },
// ...
}
ここでは、ErrFunc
が常に "bla"
という文字列と nil
エラーを返すように定義されています。
新しいテストケースが execTests
スライスに追加されました:
var execTests = []execTest{
// ...
{".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
// ...
}
".ErrFunc"
: テストケースの名前。{{call .ErrFunc}}
: 実行されるテンプレート文字列。tVal
のErrFunc
を呼び出します。"bla"
: 期待される出力。ErrFunc
が"bla"
とnil
エラーを返すため、テンプレートは"bla"
を出力するはずです。tVal
: テンプレートに渡されるデータ。true
: テストが成功することを期待することを示します。
このテストケースは、call
ビルトインが nil
エラーを返す関数を正しく処理し、パニックを起こさずに期待される文字列を返すことを検証します。
関連リンク
- Go
text/template
パッケージのドキュメント: https://pkg.go.dev/text/template - Go
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect reflect.Value.IsNil()
のドキュメント: https://pkg.go.dev/reflect#Value.IsNil
参考にした情報源リンク
- Go CL 12804043: https://golang.org/cl/12804043
- GitHub Commit: https://github.com/golang/go/commit/83348a13fb40ac80e2587e27c29d18360177f3b1