[インデックス 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