[インデックス 18536] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtext/template
パッケージにおける、関数呼び出しがnilポインタを評価した際にパニック(panic)を引き起こすバグを修正するものです。具体的には、ポインタのデリファレンス時にそのポインタがnilであった場合、以前はランタイムパニックが発生していましたが、この修正により、よりユーザーフレンドリーなエラーメッセージが返されるようになりました。
コミット
commit 71575a97ab085695e1debd371fb3b33671cd810a
Author: Rob Pike <r@golang.org>
Date: Fri Feb 14 16:26:47 2014 -0800
text/template: don't panic when function call evaluates a nil pointer
Catch the error instead and return it to the user. Before this fix,
the template package panicked. Now you get:
template: bug11:1:14: executing "bug11" at <.PS>: dereference of nil pointer of type *string
Extended example at http://play.golang.org/p/uP6pCW3qKT
Fixes #7333.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/64150043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/71575a97ab085695e1debd371fb3b33671cd810a
元コミット内容
text/template: don't panic when function call evaluates a nil pointer
このコミットの目的は、text/template
パッケージにおいて、関数呼び出しがnilポインタを評価する際にパニックを起こさないようにすることです。以前は、この状況でテンプレートパッケージがパニックを起こしていましたが、この修正により、エラーを捕捉し、ユーザーに返すようになりました。修正後は、以下のようなエラーメッセージが表示されるようになります。
template: bug11:1:14: executing "bug11" at <.PS>: dereference of nil pointer of type *string
この変更の拡張された例は、http://play.golang.org/p/uP6pCW3qKT
で確認できます。
このコミットは、Issue #7333を修正します。
変更の背景
Go言語のtext/template
パッケージは、テキストベースの出力を生成するための強力なツールです。ウェブアプリケーションのHTML生成や、設定ファイルの動的な生成など、様々な用途で利用されます。テンプレートエンジンは、データ構造(通常はGoの構造体やマップ)をテンプレートにバインドし、そのデータに基づいて出力を生成します。
このコミットが修正する問題は、テンプレート内で関数を呼び出す際に、その関数の引数としてnilポインタが渡され、かつその関数がnilポインタをデリファレンスしようとした場合に発生していました。Goのランタイムは、nilポインタのデリファレンスを検出すると、デフォルトでパニックを引き起こします。これは、プログラムの異常終了を意味し、特にサーバーアプリケーションなどではサービス停止につながる重大な問題です。
ユーザーがテンプレートを記述する際、意図せずnilポインタを渡してしまう可能性は常にあります。例えば、データソースから取得した値が期待通りに存在せず、nilになってしまうケースなどが考えられます。このような場合、テンプレートエンジンはパニックするのではなく、エラーとして適切に処理し、ユーザーにその問題を通知するべきです。これにより、開発者は問題を特定しやすくなり、アプリケーションの堅牢性が向上します。
Issue #7333は、まさにこの問題、すなわちtext/template
がnilポインタのデリファレンスでパニックするという報告でした。このコミットは、このユーザー報告に対応し、テンプレートエンジンのエラーハンドリングを改善することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とtext/template
パッケージの動作に関する知識が必要です。
-
Goのポインタとnil:
- Go言語では、ポインタは変数のメモリアドレスを保持します。
- ポインタがどのメモリアドレスも指していない状態を
nil
と呼びます。 nil
ポインタをデリファレンス(ポインタが指す値にアクセスしようとすること)しようとすると、Goランタイムは「ランタイムパニック」を引き起こします。これは、プログラムの実行を停止させる回復不可能なエラーです。
-
reflect
パッケージ:- Goの
reflect
パッケージは、実行時にプログラムの構造(型、値、メソッドなど)を検査・操作するための機能を提供します。 text/template
パッケージは、テンプレートに渡されたデータ(Goの構造体やマップ)を動的に処理するためにreflect
パッケージを extensively に使用します。例えば、テンプレート内で.Field
のようにフィールドにアクセスしたり、{{func .Arg}}
のように関数を呼び出したりする際に、reflect
を使ってそのフィールドや関数の情報を取得し、値を操作します。reflect.Value
: 任意のGoの値を表現する型です。この型を通じて、値の型、種類(Kind)、メソッドなどを取得したり、値を設定したりできます。reflect.Value.IsValid()
:reflect.Value
が有効な値を表しているかどうかをチェックします。例えば、nilポインタのデリファレンス結果や、存在しないマップキーへのアクセス結果などはIsValid()
がfalse
を返します。reflect.Value.Elem()
: ポインタが指す要素のreflect.Value
を返します。もしポインタがnil
であれば、Elem()
はIsValid()
がfalse
を返すreflect.Value
を返します。
- Goの
-
text/template
パッケージの実行モデル:text/template
は、テンプレート文字列を解析し、AST(抽象構文木)を構築します。- テンプレートの実行時には、このASTをトラバースし、データとバインドしながら出力を生成します。
- テンプレート内のアクション(例:
{{.Field}}
,{{func .Arg}}
)が評価される際、reflect
パッケージが使用され、対応するGoの値が取得・操作されます。 - エラーハンドリング: テンプレートの実行中にエラーが発生した場合、
text/template
は通常、error
型を返すか、state.errorf
のような内部エラー報告メカニズムを通じてエラーを記録します。パニックは、通常、予期せぬ、回復不能な状況でのみ発生すべきです。
技術的詳細
このコミットの技術的な核心は、text/template
パッケージのexec.go
ファイル内のvalidateType
メソッドにあります。このメソッドは、テンプレートの実行中に値の型を検証し、必要に応じて型変換を行う役割を担っています。特に、ポインタのデリファレンスを伴う操作(例: *T
型の値が期待されるが、**T
型の値が渡された場合など)において、そのポインタが有効であるかどうかのチェックが不足していました。
修正前のコードでは、value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ)
という条件が真の場合、つまりvalue
がポインタであり、そのポインタが指す要素の型が期待されるtyp
に割り当て可能である場合に、value = value.Elem()
としてポインタをデリファレンスしていました。しかし、このvalue.Elem()
の呼び出し結果がnil
ポインタをデリファレンスした結果として無効なreflect.Value
(IsValid()
がfalse
を返す)になる可能性が考慮されていませんでした。
修正後のコードでは、value = value.Elem()
の直後にif !value.IsValid() { s.errorf("dereference of nil pointer of type %s", typ) }
というチェックが追加されました。
value.Elem()
が呼び出された後、その結果がIsValid()
メソッドによってチェックされます。- もし
IsValid()
がfalse
を返した場合、それは元のポインタがnil
であったことを意味します。 - この場合、
s.errorf
メソッドが呼び出され、"dereference of nil pointer of type %s"
というフォーマットでエラーメッセージが生成され、テンプレートの実行コンテキストにエラーとして記録されます。これにより、パニックを回避し、代わりにユーザーに明確なエラーメッセージが提供されます。
この変更は、text/template
が内部的にreflect
パッケージをどのように利用しているかを深く理解していることを示しています。reflect.Value.Elem()
がnil
ポインタに対して呼び出されたときにパニックしないというreflect
パッケージの設計(代わりにIsValid()
がfalse
を返すreflect.Value
を返す)を利用して、テンプレートエンジン側でエラーを適切に捕捉しています。
テストケースexec_test.go
では、bug11
という新しいテストケースが追加されています。このテストケースは、{{valueString .PS}}
というテンプレートを使用し、.PS
がnilポインタである状況をシミュレートしています。valueString
関数は文字列を引数に取りますが、.PS
が*string
型のnilポインタであるため、テンプレートエンジンがこれをstring
型に変換しようとする際に問題が発生していました。このテストケースの追加により、修正が正しく機能し、パニックではなくエラーが返されることが検証されます。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の2つのファイルにあります。
-
src/pkg/text/template/exec.go
:func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value
メソッド内。- ポインタのデリファレンス処理の直後に、デリファレンス結果が有効な値であるかどうかのチェックが追加されました。
--- a/src/pkg/text/template/exec.go +++ b/src/pkg/text/template/exec.go @@ -594,6 +594,9 @@ func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Valu switch { case value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ): value = value.Elem() + if !value.IsValid() { + s.errorf("dereference of nil pointer of type %s", typ) + } case reflect.PtrTo(value.Type()).AssignableTo(typ) && value.CanAddr(): value = value.Addr() default:
-
src/pkg/text/template/exec_test.go
:- 新しいテストケース
bug11
がexecTests
スライスに追加されました。 valueString
という新しいヘルパー関数が追加されました。testExecute
関数内のFuncMap
にvalueString
が追加されました。
--- a/src/pkg/text/template/exec_test.go +++ b/src/pkg/text/template/exec_test.go @@ -512,6 +512,8 @@ var execTests = []execTest{\ {\"bug9\", \"{{.cause}}\", \"neglect\", map[string]string{\"cause\": \"neglect\"}, true},\ // Field chain starting with function did not work.\ {\"bug10\", \"{{mapOfThree.three}}-{{(mapOfThree).three}}\", \"3-3\", 0, true},\ + // Dereferencing nil pointer while evaluating function arguments should not panic. Issue 7333. + {\"bug11\", \"{{valueString .PS}}\", \"\", T{}, false},\ }\ \ func zeroArgs() string {\ @@ -546,6 +548,11 @@ func vfunc(V, *V) string {\ return \"vfunc\"\ }\ \ +// valueString takes a string, not a pointer. +func valueString(v string) string {\ + return \"value is ignored\"\ +}\ +\ func add(args ...int) int {\ sum := 0\ for _, x := range args {\ @@ -580,17 +587,18 @@ func mapOfThree() interface{} {\ func testExecute(execTests []execTest, template *Template, t *testing.T) {\ b := new(bytes.Buffer)\ funcs := FuncMap{\ -\t\t\"add\": add,\ -\t\t\"count\": count,\ -\t\t\"dddArg\": dddArg,\ -\t\t\"echo\": echo,\ -\t\t\"makemap\": makemap,\ -\t\t\"mapOfThree\": mapOfThree,\ -\t\t\"oneArg\": oneArg,\ -\t\t\"stringer\": stringer,\ -\t\t\"typeOf\": typeOf,\ -\t\t\"vfunc\": vfunc,\ -\t\t\"zeroArgs\": zeroArgs,\ +\t\t\"add\": add,\ +\t\t\"count\": count,\ +\t\t\"dddArg\": dddArg,\ +\t\t\"echo\": echo,\ +\t\t\"makemap\": makemap,\ +\t\t\"mapOfThree\": mapOfThree,\ +\t\t\"oneArg\": oneArg,\ +\t\t\"stringer\": stringer,\ +\t\t\"typeOf\": typeOf,\ +\t\t\"valueString\": valueString,\ +\t\t\"vfunc\": vfunc,\ +\t\t\"zeroArgs\": zeroArgs,\ }\ for _, test := range execTests {\ var tmpl *Template
- 新しいテストケース
コアとなるコードの解説
src/pkg/text/template/exec.go
の変更
validateType
関数は、テンプレートエンジンがGoの値を処理する際に、期待される型と実際の値の型が一致するかどうかを検証し、必要に応じて型変換を行うための重要な内部関数です。
変更が加えられた箇所は、value
がポインタ型であり、その要素がtyp
に割り当て可能であると判断されたブロック内です。
case value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ):
value = value.Elem()
if !value.IsValid() {
s.errorf("dereference of nil pointer of type %s", typ)
}
value = value.Elem()
: ここで、value
がポインタであるため、そのポインタが指す実際の値(要素)を取得しようとします。reflect
パッケージの設計上、もしvalue
がnil
ポインタを表すreflect.Value
であったとしても、Elem()
を呼び出してもパニックは発生しません。代わりに、IsValid()
がfalse
を返すような「ゼロ値」のreflect.Value
が返されます。if !value.IsValid()
: この行が追加された主要な変更点です。value.Elem()
の呼び出し結果が有効な値であるかどうかをチェックします。- もし
value.Elem()
がnil
ポインタのデリファレンス結果として無効なreflect.Value
を返した場合(つまり、元のポインタがnil
だった場合)、!value.IsValid()
はtrue
になります。 - この条件が真になった場合、
s.errorf(...)
が呼び出されます。s.errorf
は、text/template
パッケージの内部エラー報告メカニズムであり、指定されたフォーマット文字列と引数を使用してエラーメッセージを生成し、テンプレートの実行コンテキストにエラーとして記録します。これにより、テンプレートの実行はパニックすることなく、エラーとして終了するか、エラーが適切に処理されるようになります。
- もし
この修正により、テンプレートエンジンは、nilポインタのデリファレンスという一般的なプログラミングエラーに対して、より堅牢で予測可能な振る舞いをするようになりました。
src/pkg/text/template/exec_test.go
の変更
テストファイルには、この修正を検証するための新しいテストケースが追加されました。
-
valueString
関数の追加:// valueString takes a string, not a pointer. func valueString(v string) string { return "value is ignored" }
この関数は、テンプレート内で呼び出されることを想定したヘルパー関数です。引数として
string
型を期待します。 -
execTests
へのbug11
テストケースの追加:// Dereferencing nil pointer while evaluating function arguments should not panic. Issue 7333. {"bug11", "{{valueString .PS}}", "", T{}, false},
"bug11"
: テストケースの名前。"{{valueString .PS}}"
: 実行されるテンプレート文字列。ここで.PS
は、テストデータT{}
のフィールドであり、このテストでは*string
型のnilポインタとして設定されます。valueString
関数はstring
型を期待するため、テンプレートエンジンは.PS
をデリファレンスしてstring
型に変換しようとします。""
: 期待される出力。このテストはエラーが発生することを期待しているため、出力は空文字列です。T{}
: テンプレートに渡されるデータ。T
構造体は、PS *string
フィールドを持つと仮定されます(テストコード全体を見ないと正確な定義は不明ですが、文脈から推測できます)。このテストでは、PS
がnil
ポインタとして渡されます。false
: このテストがエラーを発生させることを期待していることを示します(true
は成功を期待)。
-
FuncMap
へのvalueString
の追加:testExecute
関数内で、valueString
関数がテンプレートエンジンに認識されるようにFuncMap
に追加されています。
これらのテストの追加により、text/template
がnilポインタをデリファレンスしようとした際にパニックせず、代わりに適切なエラーメッセージを生成することが保証されます。
関連リンク
- Go Issue #7333: https://github.com/golang/go/issues/7333
- Go CL 64150043: https://golang.org/cl/64150043
- Go Playgroundの例 (コミットメッセージに記載): http://play.golang.org/p/uP6pCW3qKT
参考にした情報源リンク
- Go言語公式ドキュメント:
text/template
パッケージ: https://pkg.go.dev/text/template - Go言語公式ドキュメント:
reflect
パッケージ: https://pkg.go.dev/reflect - Go言語におけるポインタ: https://go.dev/tour/moretypes/1
- Go言語におけるパニックと回復: https://go.dev/blog/defer-panic-and-recover
- Go言語の
reflect.Value.IsValid()
の挙動に関する情報 (一般的なGoの反射に関する記事やドキュメント)