[インデックス 17015] ファイルの概要
このコミットは、Go言語の標準ライブラリである html/template パッケージにおける、nil 値の間接参照時の挙動を修正するものです。具体的には、テンプレートエンジンが値の間接参照(ポインタのデリファレンスなど)を行う際に、入力が nil である場合にパニックを起こす可能性があった問題を解決します。
コミット
commit 53d9b6fcf3d459c2e550238502b499c462983329
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Sun Aug 4 08:41:19 2013 +1000
html/template: handle nils during indirection
Fixes #5982.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/12387043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/53d9b6fcf3d459c2e550238502b499c462983329
元コミット内容
html/template: handle nils during indirection
このコミットは、html/template パッケージが値の間接参照を行う際に nil を適切に処理するようにします。これにより、Issue #5982 で報告された問題が修正されます。
変更の背景
Go言語の html/template パッケージは、HTMLコンテンツを安全に生成するためのテンプレートエンジンを提供します。このパッケージは、テンプレート内で参照されるデータ構造のフィールドにアクセスする際に、ポインタのデリファレンス(間接参照)を自動的に行います。
Issue #5982(Go issue tracker上で報告された問題)は、html/template が nil のインターフェース値やポインタを間接参照しようとした際に、予期せぬパニック(ランタイムエラー)が発生するというバグを報告していました。特に、空ではないインターフェース型(例: error 型)に nil が代入されている場合、テンプレートエンジンがその値を処理しようとすると、内部で reflect パッケージを使った間接参照処理が nil ポインタデリファレンスを引き起こし、プログラムがクラッシュする可能性がありました。
この問題は、テンプレートの堅牢性を損なうものであり、ユーザーが意図せず nil 値をテンプレートに渡した場合にアプリケーションが停止してしまうことを意味します。そのため、nil 値が安全に処理され、パニックが発生しないように修正する必要がありました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と html/template パッケージの動作に関する知識が必要です。
-
インターフェース (Interfaces): Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。Goのインターフェースは、型と値のペアとして内部的に表現されます。
- nilインターフェース: インターフェース変数が
nilであるのは、そのインターフェースの「型」と「値」の両方がnilである場合です。 - 非nilインターフェースにnil値: インターフェース変数が
nilではないが、その内部の「値」がnilである場合があります。これは、具体的な型がnilポインタである場合によく発生します(例:var err error = (*MyError)(nil))。この場合、インターフェース自体はnilではないため、if err != nilはtrueになりますが、内部の値はnilです。この状態が、今回の問題の根本原因でした。
- nilインターフェース: インターフェース変数が
-
リフレクション (Reflection):
reflectパッケージは、実行時にプログラムの構造を検査・操作するための機能を提供します。html/templateは、テンプレートに渡されたデータの型や値を動的に調べるためにリフレクションを多用します。reflect.ValueOf(a): 任意のGoの値をreflect.Value型に変換します。v.Kind():reflect.Valueの基底型(Ptr,Struct,Intなど)を返します。v.Elem(): ポインタが指す要素のreflect.Valueを返します。ポインタがnilの場合、このメソッドを呼び出すとパニックが発生します。v.IsNil():reflect.Valueがnilであるかどうかを判定します。これは、ポインタ、インターフェース、マップ、スライス、チャネル、関数に対してのみ有効です。
-
html/templateパッケージ:- データバインディング: テンプレートは、Goの構造体やマップなどのデータソースにバインドされ、そのフィールドの値がテンプレート内で表示されます。
- 間接参照 (Indirection): テンプレートエンジンは、データソースのフィールドがポインタである場合、自動的にそのポインタをデリファレンスして、実際の値にアクセスしようとします。これは、
indirectやindirectToStringerOrErrorといった内部関数によって行われます。 fmt.Stringerインターフェース:fmt.Stringerインターフェースを実装する型は、String()メソッドを提供することで、その型の文字列表現をカスタマイズできます。html/templateは、値を文字列として表示する際にこのインターフェースを尊重します。
今回の問題は、特に「非nilインターフェースにnil値」が代入されている場合に、reflect.Value.Elem() を呼び出す前に reflect.Value.IsNil() で適切にチェックが行われていなかったために発生しました。
技術的詳細
このコミットは、html/template パッケージ内の2つの重要なヘルパー関数 indirect と indirectToStringerOrError に nil チェックを追加することで問題を解決しています。
indirect 関数
indirect 関数は、与えられた値 a を、ポインタのデリファレンスを繰り返して基底の型(または nil)に到達するまで間接参照する役割を担います。
変更前:
func indirect(a interface{}) interface{} {
if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
// Avoid creating a reflect.Value if it's not a pointer.
return a
}
v := reflect.ValueOf(a)
for v.Kind() == reflect.Ptr && !v.IsNil() { // ここで v.IsNil() はポインタがnilの場合のみチェック
v = v.Elem()
}
return v.Interface()
}
変更前のコードでは、reflect.TypeOf(a) がポインタ型でない場合はすぐに a を返していました。しかし、a がインターフェース型で、そのインターフェースが nil ではないが、内部の値が nil ポインタである場合(例: var e error = (*MyError)(nil))、reflect.TypeOf(a).Kind() は reflect.Interface を返します。この場合、reflect.Ptr ではないため、a がそのまま返されます。
問題は、a がポインタ型の場合、reflect.ValueOf(a) で reflect.Value を取得し、ループ内で v.Elem() を呼び出す際に発生しました。v.IsNil() は reflect.Value が nil ポインタであるかをチェックしますが、a がインターフェース型で、そのインターフェースが nil ではないが、内部の値が nil ポインタである場合、v.Kind() == reflect.Ptr は false になり、ループに入りません。しかし、indirectToStringerOrError の方で問題が発生していました。
変更後:
func indirect(a interface{}) interface{} {
if a == nil { // 新しく追加されたnilチェック
return nil
}
if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
// Avoid creating a reflect.Value if it's not a pointer.
return a
}
v := reflect.ValueOf(a)
for v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}
return v.Interface()
}
この変更により、関数の一番最初に a == nil のチェックが追加されました。これにより、a が完全に nil である場合(インターフェースの型と値の両方が nil の場合)は、すぐに nil を返すようになります。これは、html/template が nil 値を適切に処理するための基本的なガードです。
indirectToStringerOrError 関数
indirectToStringerOrError 関数は indirect と似ていますが、fmt.Stringer または error インターフェースの実装に到達するまで間接参照を続けます。これは、テンプレートが値を文字列として表示する際に使用されます。
変更前:
func indirectToStringerOrError(a interface{}) interface{} {
v := reflect.ValueOf(a)
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}
return v.Interface()
}
この関数では、a がインターフェース型で、そのインターフェースが nil ではないが、内部の値が nil ポインタである場合(例: var e error = (*MyError)(nil))、v.Kind() == reflect.Ptr は true になります。しかし、v.IsNil() は false を返します(インターフェース自体は nil ではないため)。この状態で v.Elem() を呼び出すと、nil ポインタのデリファレンスが発生し、パニックを引き起こしていました。
変更後:
func indirectToStringerOrError(a interface{}) interface{} {
if a == nil { // 新しく追加されたnilチェック
return nil
}
v := reflect.ValueOf(a)
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}
return v.Interface()
}
indirect 関数と同様に、indirectToStringerOrError 関数にも a == nil のチェックが追加されました。これにより、関数が受け取った a が完全に nil である場合に、安全に nil を返すようになります。
この修正のポイントは、reflect.ValueOf(a) を呼び出す前に a == nil をチェックすることです。これにより、reflect.ValueOf(nil) が reflect.Value のゼロ値を返すというGoのリフレクションの挙動とは別に、明示的に nil を処理することで、後続の reflect.Value の操作で予期せぬパニックが発生するのを防ぎます。特に、nil のインターフェース値が渡された場合に、reflect.ValueOf が返す Value オブジェクトが IsValid() は false だが IsNil() は true となるような複雑なケースを避けるため、早期リターンが有効です。
また、このコミットでは、TestEscapingNilNonemptyInterfaces という新しいテストケースが追加されています。このテストは、error 型のような空ではないインターフェースに nil 値が代入された場合に、テンプレートがパニックを起こさずに正しく処理できることを検証します。これは、今回の修正が意図した通りに機能していることを保証するための重要なテストです。
コアとなるコードの変更箇所
src/pkg/html/template/content.go ファイルの indirect 関数と indirectToStringerOrError 関数に nil チェックが追加されました。
--- a/src/pkg/html/template/content.go
+++ b/src/pkg/html/template/content.go
@@ -74,6 +74,9 @@ const (
// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
func indirect(a interface{}) interface{} {
+ if a == nil {
+ return nil
+ }
if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
// Avoid creating a reflect.Value if it's not a pointer.
return a
@@ -94,6 +97,9 @@ var (
// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer
// or error,
func indirectToStringerOrError(a interface{}) interface{} {
+ if a == nil {
+ return nil
+ }
v := reflect.ValueOf(a)
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
src/pkg/html/template/content_test.go ファイルに新しいテストケース TestEscapingNilNonemptyInterfaces が追加されました。
--- a/src/pkg/html/template/content_test.go
+++ b/src/pkg/html/template/content_test.go
@@ -259,3 +259,28 @@ func TestStringer(t *testing.T) {
t.Errorf("expected %q got %q", expect, b.String())
}
}
+
+// https://code.google.com/p/go/issues/detail?id=5982
+func TestEscapingNilNonemptyInterfaces(t *testing.T) {
+ tmpl := Must(New("x").Parse("{{.E}}"))
+
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("panic during template execution: %v", r)
+ }
+ }()
+
+ got := new(bytes.Buffer)
+ testData := struct{ E error }{} // any non-empty interface here will do; error is just ready at hand
+ tmpl.Execute(got, testData)
+
+ // Use this data instead of just hard-coding "<nil>" to avoid
+ // dependencies on the html escaper and the behavior of fmt w.r.t. nil.
+ want := new(bytes.Buffer)
+ data := struct{ E string }{E: fmt.Sprint(nil)}
+ tmpl.Execute(want, data)
+
+ if !bytes.Equal(want.Bytes(), got.Bytes()) {
+ t.Errorf("expected %q got %q", string(want.Bytes()), string(got.Bytes()))
+ }
+}
コアとなるコードの解説
content.go の変更
indirect および indirectToStringerOrError 関数の冒頭に以下の行が追加されました。
if a == nil {
return nil
}
このシンプルな nil チェックは、関数に渡されたインターフェース値 a が完全に nil である場合(つまり、型と値の両方が nil である場合)に、それ以上処理を進めずに nil を返すようにします。これにより、後続のリフレクション操作(特に reflect.ValueOf(a) やその後の v.Elem())が nil ポインタデリファレンスを引き起こす可能性を排除します。
content_test.go の変更
TestEscapingNilNonemptyInterfaces テストは、Issue #5982 で報告された具体的なシナリオを再現し、修正が正しく機能することを確認します。
func TestEscapingNilNonemptyInterfaces(t *testing.T) {
tmpl := Must(New("x").Parse("{{.E}}"))
defer func() {
if r := recover(); r != nil {
t.Errorf("panic during template execution: %v", r)
}
}()
got := new(bytes.Buffer)
testData := struct{ E error }{} // any non-empty interface here will do; error is just ready at hand
tmpl.Execute(got, testData)
// Use this data instead of just hard-coding "<nil>" to avoid
// dependencies on the html escaper and the behavior of fmt w.r.t. nil.
want := new(bytes.Buffer)
data := struct{ E string }{E: fmt.Sprint(nil)}
tmpl.Execute(want, data)
if !bytes.Equal(want.Bytes(), got.Bytes()) {
t.Errorf("expected %q got %q", string(want.Bytes()), string(got.Bytes()))
}
}
このテストの重要な点は以下の通りです。
testData := struct{ E error }{}:Eフィールドがerror型(空ではないインターフェース)であり、明示的に値が代入されていないため、その値はnilとなります。しかし、testData.Eはnilインターフェースではなく、内部の値がnilポインタであるインターフェースとなります。これが、以前のバージョンでパニックを引き起こした原因です。defer func() { if r := recover(); r != nil { ... } }(): このdeferステートメントは、テンプレートの実行中にパニックが発生した場合にそれを捕捉し、テストを失敗させることで、パニックが発生しないことを保証します。tmpl.Execute(got, testData):nil値を持つerrorインターフェースをテンプレートに渡して実行します。wantの生成:fmt.Sprint(nil)の結果(通常は<nil>)を期待値として使用することで、html/templateのエスケープ処理やfmtパッケージのnil処理に依存しない、より堅牢な比較を行っています。
このテストは、html/template が nil のインターフェース値を安全に処理し、パニックを起こさずに期待される文字列(通常は <nil>)を出力できることを確認します。
関連リンク
- Go Issue 5982: https://code.google.com/p/go/issues/detail?id=5982 (このコミットが修正した問題の報告)
- Go Code Review 12387043: https://golang.org/cl/12387043 (このコミットのコードレビューページ)
参考にした情報源リンク
- Go Issue 5982: https://code.google.com/p/go/issues/detail?id=5982
- Go Code Review 12387043: https://golang.org/cl/12387043
- Go言語の
reflectパッケージに関する公式ドキュメント: https://pkg.go.dev/reflect - Go言語の
html/templateパッケージに関する公式ドキュメント: https://pkg.go.dev/html/template - Go言語のインターフェースに関する解説(例: A Tour of Go - Interfaces): https://go.dev/tour/methods/9
- Go言語における
nilとインターフェースの挙動に関する一般的な情報源(例: "The Laws of Reflection" by Rob Pike): https://go.dev/blog/laws-of-reflection