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

[インデックス 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/templatecall ビルトインは、関数の2番目の戻り値がエラー型であると仮定し、無条件にその値を error インターフェースに型アサートしようとします。しかし、Goにおいて nil は特定の型を持たないため、nilerror インターフェースに型アサートしようとすると、ランタイムパニック(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のランタイムは「nilerror インターフェースではない」と判断し、パニックを引き起こします。

このコミットでは、この問題に対処するために、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 パッケージの実行に関するテストが含まれています。

新しいフィールド ErrFuncT 構造体に追加されました:

type T struct {
    // ...
    ErrFunc         func() (string, error)
    // ...
}

これは、テストで使用する構造体 T に、stringerror を返す関数を保持するためのフィールドを追加します。

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}}: 実行されるテンプレート文字列。tValErrFunc を呼び出します。
  • "bla": 期待される出力。ErrFunc"bla"nil エラーを返すため、テンプレートは "bla" を出力するはずです。
  • tVal: テンプレートに渡されるデータ。
  • true: テストが成功することを期待することを示します。

このテストケースは、call ビルトインが nil エラーを返す関数を正しく処理し、パニックを起こさずに期待される文字列を返すことを検証します。

関連リンク

参考にした情報源リンク