[インデックス 10161] ファイルの概要
このコミットは、Go言語のtemplate
パッケージにおいて、テンプレートが正常にパース(解析)される前にExecute
メソッドが呼び出された場合に発生するエラー処理を改善するものです。以前はパニック(panic)を引き起こしていましたが、この変更により、より適切なエラーメッセージを返すように修正されました。これにより、開発者はテンプレートの利用における問題をより明確に把握できるようになります。
コミット
commit cae23f036ac639141153a73551bcabbb7169e9e0
Author: Scott Lawrence <bytbox@gmail.com>
Date: Mon Oct 31 16:07:17 2011 -0700
template: fix error checking on execute without parse
Fixed error checking in exec.go to give a sensible error message when
execution is attempted before a successful parse (rather than an
outright panic).
R=r
CC=golang-dev
https://golang.org/cl/5306065
---
src/pkg/exp/template/html/escape_test.go | 12 ++++++------
src/pkg/template/exec.go | 2 +-\n 2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/pkg/exp/template/html/escape_test.go b/src/pkg/exp/template/html/escape_test.go
index a4ea7596cd..1b3b256733 100644
--- a/src/pkg/exp/template/html/escape_test.go
+++ b/src/pkg/exp/template/html/escape_test.go
@@ -1549,8 +1549,8 @@ func TestEnsurePipelineContains(t *testing.T) {
}\n }\n \n-func expectExecuteFailure(t *testing.T, b *bytes.Buffer) {\n-\tif x := recover(); x != nil {\n+func expectExecuteFailure(t *testing.T, b *bytes.Buffer, err os.Error) {\n+\tif err != nil {\n \t\tif b.Len() != 0 {\n \t\t\tt.Errorf(\"output on buffer: %q\", b.String())\n \t\t}\n@@ -1563,8 +1563,8 @@ func TestEscapeErrorsNotIgnorable(t *testing.T) {\n \tvar b bytes.Buffer\n \ttmpl := template.Must(template.New(\"dangerous\").Parse(\"<a\"))\n \tEscape(tmpl)\n-\tdefer expectExecuteFailure(t, &b)\n-\ttmpl.Execute(&b, nil)\n+\terr := tmpl.Execute(&b, nil)\n+\texpectExecuteFailure(t, &b, err)\n }\n \n func TestEscapeSetErrorsNotIgnorable(t *testing.T) {\n@@ -1574,8 +1574,8 @@ func TestEscapeSetErrorsNotIgnorable(t *testing.T) {\n \t}\n \tEscapeSet(s, \"t\")\n \tvar b bytes.Buffer\n-\tdefer expectExecuteFailure(t, &b)\n-\ts.Execute(&b, \"t\", nil)\n+\terr = s.Execute(&b, \"t\", nil)\n+\texpectExecuteFailure(t, &b, err)\n }\n \n func TestRedundantFuncs(t *testing.T) {\ndiff --git a/src/pkg/template/exec.go b/src/pkg/template/exec.go
index e7fad72fe7..34c6633232 100644
--- a/src/pkg/template/exec.go
+++ b/src/pkg/template/exec.go
@@ -97,7 +97,7 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err os.Error) {\n \t\tline: 1,\n \t\tvars: []variable{{\"$\", value}},\n \t}\n-\tif t.Root == nil {\n+\tif t.Tree == nil || t.Root == nil {\n \t\tstate.errorf(\"must be parsed before execution\")\n \t}\n \tstate.walk(value, t.Root)\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/cae23f036ac639141153a73551bcabbb7169e9e0](https://github.com/golang/go/commit/cae23f036ac639141153a73551bcabbb7169e9e0)
## 元コミット内容
このコミットは、`template`パッケージにおいて、テンプレートがパースされる前に`Execute`が呼び出された際のエラーチェックを修正するものです。これにより、パニックではなく、より適切なエラーメッセージが返されるようになります。
## 変更の背景
Go言語の`template`パッケージ(当時は`html/template`や`text/template`として知られていた)は、動的なコンテンツ生成に広く利用されています。テンプレートを使用する一般的なワークフローは、まずテンプレート文字列を`Parse`メソッドで解析し、その後に`Execute`メソッドでデータと結合して最終的な出力を生成するというものです。
しかし、このコミット以前のバージョンでは、`Parse`が成功する前に`Execute`が呼び出された場合、プログラムがパニック(panic)を起こしていました。パニックはGo言語における回復不可能なエラーを示すもので、通常はプログラムの異常終了を意味します。ライブラリの利用者にとっては、予期せぬパニックはデバッグを困難にし、アプリケーションの安定性を損なう原因となります。
この変更の背景には、より堅牢でユーザーフレンドリーなエラーハンドリングの実現があります。パニックではなく、明確なエラーメッセージを返すことで、開発者はテンプレートの利用順序の誤りを容易に特定し、修正できるようになります。これは、ライブラリの使いやすさと信頼性を向上させるための重要な改善です。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と`template`パッケージの基本的な知識が必要です。
### Go言語におけるエラーハンドリング (`error`型と`panic`/`recover`)
* **`error`型**: Go言語では、エラーは組み込みの`error`インターフェースによって表現されます。関数は通常、最後の戻り値として`error`を返します。呼び出し元は、この`error`が`nil`でない場合にエラーが発生したと判断し、適切に処理します。これはGo言語における推奨されるエラーハンドリングのメカニズムです。
* **`panic`と`recover`**: `panic`は、プログラムが回復不可能な状態に陥ったことを示すために使用されます。`panic`が発生すると、現在の関数の実行が停止し、遅延関数(`defer`で登録された関数)が実行され、その後呼び出しスタックを遡ってパニックが伝播します。最終的に、パニックが`recover`によって捕捉されない場合、プログラムは異常終了します。`recover`は`defer`関数内で呼び出され、パニックから回復するために使用されます。このコミットの変更前は、テンプレートがパースされていない状態で`Execute`が呼ばれると`panic`が発生していました。
### `template`パッケージの基本
Go言語の`html/template`および`text/template`パッケージは、データとテンプレートを組み合わせてHTMLやテキストを生成するための機能を提供します。
* **`template.Template`構造体**: テンプレートのインスタンスを表します。
* **`template.New(name string)`**: 新しいテンプレートインスタンスを作成します。
* **`tmpl.Parse(text string)`**: テンプレート文字列を解析し、テンプレートの内部表現(構文木)を構築します。この操作が成功すると、`Template`インスタンスの`Tree`フィールドや`Root`フィールドに解析結果が格納されます。
* **`tmpl.Execute(wr io.Writer, data interface{})`**: 解析されたテンプレートにデータを適用し、結果を指定された`io.Writer`に書き込みます。このメソッドは、実行中にエラーが発生した場合に`error`を返します。
### `os.Error` (Go 1.0以前のエラーインターフェース)
このコミットが作成された2011年当時、Go言語のエラーインターフェースは`os.Error`という名前でした。Go 1.0のリリースに伴い、より汎用的な`error`という名前に変更されました。このコミットのコードスニペットでは`os.Error`が使用されていますが、現代のGoコードでは`error`と読み替えることができます。
### `bytes.Buffer`
`bytes.Buffer`は、可変長のバイトバッファを実装する型です。`io.Writer`インターフェースを満たすため、`template.Execute`の出力先としてよく使用されます。テストコードでは、テンプレートの出力をキャプチャするために利用されています。
## 技術的詳細
このコミットの主要な変更点は、`src/pkg/template/exec.go`ファイル内の`Template.Execute`メソッドにおけるエラーチェックの強化です。
変更前は、`Execute`メソッド内でテンプレートのルートノード(`t.Root`)が`nil`であるかどうかのみをチェックしていました。`t.Root`が`nil`の場合、テンプレートがパースされていないことを意味し、この状態で`state.errorf`を呼び出すと、内部的にパニックを引き起こしていました。
変更後は、条件式が`t.Tree == nil || t.Root == nil`に拡張されました。
* `t.Tree`: テンプレートの構文木全体を表すフィールドです。`Parse`が成功すると、このフィールドに構文木のルートが設定されます。
* `t.Root`: テンプレートの実行開始点となるルートノードです。
`t.Tree`または`t.Root`のいずれかが`nil`である場合、テンプレートが適切にパースされていないと判断されます。この場合、`state.errorf("must be parsed before execution")`が呼び出されます。`state.errorf`は、パニックを引き起こす代わりに、`Execute`メソッドの戻り値として適切な`error`を返すように修正されました。これにより、呼び出し元は`Execute`の戻り値をチェックすることで、このエラーを捕捉し、適切に処理できるようになります。
また、`src/pkg/exp/template/html/escape_test.go`ファイルでは、この変更を検証するためのテストコードが修正されています。
以前の`expectExecuteFailure`関数は、`defer recover()`を使用してパニックを捕捉していました。これは、変更前の`Execute`がパニックを引き起こすことを前提としたテストでした。
変更後は、`expectExecuteFailure`関数が`os.Error`(現在の`error`)型の引数を受け取るように変更され、`Execute`が返すエラーを直接チェックするように修正されました。これにより、テストはパニックではなく、`Execute`が返すエラーを期待するようになりました。
具体的には、以下のテストケースが修正されました。
* `TestEscapeErrorsNotIgnorable`: `template.Must(template.New("dangerous").Parse("<a"))`のように、意図的に不完全なテンプレートをパースし、その後に`Escape`を適用して`Execute`を呼び出すことで、エラーが発生することを確認しています。
* `TestEscapeSetErrorsNotIgnorable`: 複数のテンプレートを扱う`template.Set`の場合でも同様のエラーハンドリングが適用されることを確認しています。
これらのテストの修正は、`Execute`がパニックではなくエラーを返すようになったという動作変更を反映したものです。
## コアとなるコードの変更箇所
### `src/pkg/template/exec.go`
```diff
--- a/src/pkg/template/exec.go
+++ b/src/pkg/template/exec.go
@@ -97,7 +97,7 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err os.Error) {
\tline: 1,\n \t\tvars: []variable{{\"$\", value}},\n \t}\n-\tif t.Root == nil {\n+\tif t.Tree == nil || t.Root == nil {\n \t\tstate.errorf(\"must be parsed before execution\")\n \t}\n \tstate.walk(value, t.Root)\n```
### `src/pkg/exp/template/html/escape_test.go`
```diff
--- a/src/pkg/exp/template/html/escape_test.go
+++ b/src/pkg/exp/template/html/escape_test.go
@@ -1549,8 +1549,8 @@ func TestEnsurePipelineContains(t *testing.T) {
}\n }\n \n-func expectExecuteFailure(t *testing.T, b *bytes.Buffer) {\n-\tif x := recover(); x != nil {\n+func expectExecuteFailure(t *testing.T, b *bytes.Buffer, err os.Error) {\n+\tif err != nil {\n \t\tif b.Len() != 0 {\n \t\t\tt.Errorf(\"output on buffer: %q\", b.String())\n \t\t}\n@@ -1563,8 +1563,8 @@ func TestEscapeErrorsNotIgnorable(t *testing.T) {\n \tvar b bytes.Buffer\n \ttmpl := template.Must(template.New(\"dangerous\").Parse(\"<a\"))\n \tEscape(tmpl)\n-\tdefer expectExecuteFailure(t, &b)\n-\ttmpl.Execute(&b, nil)\n+\terr := tmpl.Execute(&b, nil)\n+\texpectExecuteFailure(t, &b, err)\n }\n \n func TestEscapeSetErrorsNotIgnorable(t *testing.T) {\n@@ -1574,8 +1574,8 @@ func TestEscapeSetErrorsNotIgnorable(t *testing.T) {\n \t}\n \tEscapeSet(s, \"t\")\n \tvar b bytes.Buffer\n-\tdefer expectExecuteFailure(t, &b)\n-\ts.Execute(&b, \"t\", nil)\n+\terr = s.Execute(&b, \"t\", nil)\n+\texpectExecuteFailure(t, &b, err)\n }\n \n func TestRedundantFuncs(t *testing.T) {\n```
## コアとなるコードの解説
### `src/pkg/template/exec.go` の変更
`Template.Execute`メソッドの冒頭部分で、テンプレートがパースされているかどうかのチェックが行われます。
変更前:
```go
if t.Root == nil {
state.errorf("must be parsed before execution")
}
このコードは、t.Root
がnil
の場合にエラーを発生させていました。しかし、state.errorf
が内部的にパニックを引き起こす実装であったため、テンプレートがパースされていない状態でExecute
が呼び出されると、プログラムがクラッシュしていました。
変更後:
if t.Tree == nil || t.Root == nil {
state.errorf("must be parsed before execution")
}
この変更により、チェック条件がt.Tree == nil || t.Root == nil
に強化されました。t.Tree
はテンプレートの構文木全体を指し、t.Root
はその構文木のルートノードを指します。Parse
メソッドが成功すると、これらのフィールドが適切に設定されます。したがって、どちらか一方がnil
である場合でも、テンプレートが完全にパースされていないと判断できます。
最も重要な点は、state.errorf
がパニックを引き起こすのではなく、Execute
メソッドがerror
を返すように修正されたことです。これにより、Execute
の呼び出し元は、以下のようにエラーを適切に処理できるようになります。
err := tmpl.Execute(&b, nil)
if err != nil {
// エラー処理
fmt.Println("Error executing template:", err)
}
src/pkg/exp/template/html/escape_test.go
の変更
テストファイルでは、expectExecuteFailure
関数のシグネチャと呼び出し方が変更されました。
変更前:
func expectExecuteFailure(t *testing.T, b *bytes.Buffer) {
if x := recover(); x != nil {
// ... パニックを捕捉して検証 ...
}
}
// 呼び出し例:
// defer expectExecuteFailure(t, &b)
// tmpl.Execute(&b, nil)
以前は、defer
とrecover()
を組み合わせて、tmpl.Execute
が引き起こすパニックを捕捉し、そのパニックが期待されるものであることを検証していました。
変更後:
func expectExecuteFailure(t *testing.T, b *bytes.Buffer, err os.Error) {
if err != nil {
// ... エラーを直接検証 ...
}
}
// 呼び出し例:
// err := tmpl.Execute(&b, nil)
// expectExecuteFailure(t, &b, err)
新しい実装では、expectExecuteFailure
関数はos.Error
(現在のerror
)型の引数err
を受け取ります。tmpl.Execute
の戻り値であるerr
を直接この関数に渡し、err
がnil
でないことを確認することで、エラーが適切に返されたことを検証します。これにより、テストはパニックの発生ではなく、Execute
メソッドがエラーを返すという新しい動作に適合しました。
これらの変更は、Go言語のエラーハンドリングのベストプラクティスに沿ったものであり、ライブラリの堅牢性と使いやすさを向上させています。
関連リンク
- Go CL 5306065: https://golang.org/cl/5306065
参考にした情報源リンク
- Go言語の公式ドキュメント (templateパッケージ): https://pkg.go.dev/text/template
- Go言語のエラーハンドリングに関する公式ブログ記事など (一般的な知識として参照)
- Go言語の
panic
とrecover
に関する公式ドキュメント (一般的な知識として参照) - Go言語の歴史的な変更に関する情報 (
os.Error
からerror
への変更など)