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

[インデックス 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パッケージの動作に関する知識が必要です。

  1. Goのポインタとnil:

    • Go言語では、ポインタは変数のメモリアドレスを保持します。
    • ポインタがどのメモリアドレスも指していない状態をnilと呼びます。
    • nilポインタをデリファレンス(ポインタが指す値にアクセスしようとすること)しようとすると、Goランタイムは「ランタイムパニック」を引き起こします。これは、プログラムの実行を停止させる回復不可能なエラーです。
  2. 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を返します。
  3. 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.ValueIsValid()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つのファイルにあります。

  1. 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:
    
  2. src/pkg/text/template/exec_test.go:

    • 新しいテストケースbug11execTestsスライスに追加されました。
    • valueStringという新しいヘルパー関数が追加されました。
    • testExecute関数内のFuncMapvalueStringが追加されました。
    --- 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)
			}
  1. value = value.Elem(): ここで、valueがポインタであるため、そのポインタが指す実際の値(要素)を取得しようとします。reflectパッケージの設計上、もしvaluenilポインタを表すreflect.Valueであったとしても、Elem()を呼び出してもパニックは発生しません。代わりに、IsValid()falseを返すような「ゼロ値」のreflect.Valueが返されます。
  2. 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 の変更

テストファイルには、この修正を検証するための新しいテストケースが追加されました。

  1. valueString関数の追加:

    // valueString takes a string, not a pointer.
    func valueString(v string) string {
    	return "value is ignored"
    }
    

    この関数は、テンプレート内で呼び出されることを想定したヘルパー関数です。引数としてstring型を期待します。

  2. 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フィールドを持つと仮定されます(テストコード全体を見ないと正確な定義は不明ですが、文脈から推測できます)。このテストでは、PSnilポインタとして渡されます。
    • false: このテストがエラーを発生させることを期待していることを示します(trueは成功を期待)。
  3. FuncMapへのvalueStringの追加: testExecute関数内で、valueString関数がテンプレートエンジンに認識されるようにFuncMapに追加されています。

これらのテストの追加により、text/templateがnilポインタをデリファレンスしようとした際にパニックせず、代わりに適切なエラーメッセージを生成することが保証されます。

関連リンク

参考にした情報源リンク