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