[インデックス 17632] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template/parse
および html/template
パッケージにおけるバグ修正と機能改善に関するものです。具体的には、html/template
のクローン処理中に parse.Tree
の text
フィールドが適切にコピーされないことによって発生していたパニック(panic)を解消し、テンプレートのクローン機能の堅牢性を向上させています。
コミット
commit eeb758546e10b33be161e76b3c3290dbb7a70a87
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Tue Sep 17 14:19:44 2013 +1000
text/template/parse, html/template: copy Tree.text during html template clone
The root cause of the panic reported in https://code.google.com/p/go/issues/detail?id=5980
is that parse's Tree.Text wasn't being copied during the clone.
Fix this by adding and using a Copy method for parse.Tree.
Fixes #5980.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/12420044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eeb758546e10b33be161e76b3c3290dbb7a70a87
元コミット内容
text/template/parse, html/template: htmlテンプレートのクローン時にTree.textをコピーする
https://code.google.com/p/go/issues/detail?id=5980 で報告されたパニックの根本原因は、parseのTree.Textがクローン時にコピーされていなかったことである。
parse.TreeにCopyメソッドを追加し、それを使用することでこれを修正する。
Fixes #5980.
変更の背景
このコミットは、Go言語のテンプレートエンジンにおける重要なバグを修正するために行われました。具体的には、html/template
パッケージでテンプレートをクローン(複製)する際に、内部的に使用される text/template/parse
パッケージの Tree
構造体に含まれる text
フィールドが適切にコピーされないという問題がありました。
この text
フィールドは、テンプレートのパース(解析)時に元のテンプレート文字列を保持するために使用されます。テンプレートのクローンは、既存のテンプレートを基に新しいテンプレートを作成し、その新しいテンプレートに対して追加のパース操作を行ったり、独立した変更を加えたりすることを可能にする機能です。しかし、text
フィールドがコピーされないと、クローンされたテンプレートが元のテンプレートと同じ text
フィールドを参照し続けることになります。
その結果、クローンされたテンプレートに対して Parse
メソッドが呼び出され、新しいテンプレート定義が追加されたり、既存の定義が変更されたりすると、元のテンプレートの text
フィールドも意図せず変更されてしまう可能性がありました。これが、https://code.google.com/p/go/issues/detail?id=5980
で報告されたパニックの根本原因でした。パニックは、テンプレートの実行時やさらなるパース操作時に、期待されるテンプレート構造と実際の text
フィールドの内容との間に不整合が生じることで発生したと考えられます。
この修正は、テンプレートのクローン機能が期待通りに動作し、元のテンプレートに副作用を与えないようにするために不可欠でした。
前提知識の解説
このコミットを理解するためには、以下のGo言語のテンプレートに関する知識が必要です。
-
text/template
とhtml/template
パッケージ:text/template
は、任意のテキスト形式の出力を生成するための汎用的なテンプレートエンジンです。html/template
はtext/template
をベースにしていますが、HTML出力に特化しており、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ機能を提供します。ウェブアプリケーションでHTMLを生成する際には、セキュリティ上の理由からhtml/template
の使用が強く推奨されます。- 両パッケージは、テンプレートのパースと実行のコアロジックを共有しています。
-
テンプレートのパースツリー (
parse.Tree
):- Goのテンプレートエンジンは、テンプレート文字列を解析(パース)して、内部的にツリー構造(抽象構文木、AST)を構築します。このツリーは、テンプレートの構造(アクション、パイプライン、定義など)を表現します。
text/template/parse
パッケージは、このパースツリーを構築・操作するための低レベルな機能を提供します。parse.Tree
構造体は、テンプレートの名前 (Name
)、ルートノード (Root
)、そして元のテンプレート文字列 (text
) などの情報を含んでいます。text
フィールドは、エラーメッセージのコンテキストを提供したり、デバッグ情報として利用されたりする可能性があります。
-
テンプレートのクローン (
Clone
メソッド):Template
型(text/template.Template
およびhtml/template.Template
)にはClone()
メソッドが提供されています。- このメソッドは、既存のテンプレートオブジェクトのコピーを作成します。これにより、元のテンプレートを変更せずに、そのコピーに対して新しいテンプレート定義を追加したり、既存の定義を上書きしたりすることが可能になります。これは、例えば、ベースとなるレイアウトテンプレートを定義し、それをクローンして各ページ固有のコンテンツを追加するようなシナリオで非常に有用です。
-
パニック (Panic):
- Goにおけるパニックは、プログラムの実行中に回復不可能なエラーが発生したことを示すメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、遅延関数(
defer
)が実行された後、プログラムは終了します。 - このコミットで修正されたパニックは、テンプレートの内部状態の不整合によって引き起こされたものであり、プログラムのクラッシュにつながる深刻なバグでした。
- Goにおけるパニックは、プログラムの実行中に回復不可能なエラーが発生したことを示すメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、遅延関数(
技術的詳細
このコミットの技術的な核心は、parse.Tree
構造体のディープコピー(深層コピー)を適切に行うことにあります。
元の実装では、html/template
の Clone
メソッドが parse.Tree
をクローンする際に、Tree
構造体自体は新しいインスタンスを作成していましたが、その内部フィールドである Root
(テンプレートのASTのルートノード) は CopyList()
メソッドを使ってディープコピーしていました。しかし、Tree
構造体のもう一つの重要なフィールドである text
(元のテンプレート文字列) は、単に参照がコピーされるだけでした。
この「シャローコピー(浅いコピー)」の問題により、クローンされた Tree
と元の Tree
が同じ text
フィールドを共有することになります。もしクローンされたテンプレートに対して Parse
メソッドが呼び出され、その過程で text
フィールドが変更されるような操作が行われた場合、元のテンプレートの text
フィールドも同時に変更されてしまい、結果として元のテンプレートの動作に予期せぬ影響を与える可能性がありました。これが、issue 5980
で報告されたパニックの原因でした。
このコミットでは、この問題を解決するために以下の変更が加えられました。
-
parse.Tree.Copy()
メソッドの追加:src/pkg/text/template/parse/parse.go
にfunc (t *Tree) Copy() *Tree
メソッドが追加されました。- このメソッドは、
Tree
構造体の新しいインスタンスを作成し、Name
、ParseName
、Root
(これはRoot.CopyList()
を呼び出してディープコピーされる)、そしてtext
フィールドをすべて新しいインスタンスにコピーします。これにより、Tree
オブジェクト全体のディープコピーが保証されます。
-
html/template.Template.Clone()
でのparse.Tree.Copy()
の利用:src/pkg/html/template/template.go
のTemplate.Clone()
メソッド内で、テンプレートのTree
フィールドをクローンする際に、既存のx.Tree = &parse.Tree{...}
のような手動でのフィールドコピーではなく、新しく追加されたx.Tree = x.Tree.Copy()
を呼び出すように変更されました。- これにより、
html/template
のクローン処理がparse.Tree
の完全なディープコピーを利用するようになり、text
フィールドの共有による問題が解消されました。
-
テストケースの追加:
src/pkg/html/template/clone_test.go
にTestCloneThenParse
とTestFuncMapWorksAfterClone
という新しいテストケースが追加されました。TestCloneThenParse
は、クローンされたテンプレートに新しい定義を追加しても、元のテンプレートに影響がないことを確認します。これは、Tree.text
のコピーが正しく行われていることを間接的に検証します。TestFuncMapWorksAfterClone
は、issue 5980
の具体的なシナリオを再現し、クローン後にFuncMap
が正しく機能し、パニックが発生しないことを確認します。
これらの変更により、html/template
のクローン機能はより堅牢になり、元のテンプレートとクローンされたテンプレートが完全に独立して動作することが保証されるようになりました。
コアとなるコードの変更箇所
src/pkg/html/template/template.go
--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -190,12 +190,7 @@ func (t *Template) Clone() (*Template, error) {
if src == nil || src.escaped {
return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
}
- if x.Tree != nil {
- x.Tree = &parse.Tree{
- Name: x.Tree.Name,
- Root: x.Tree.Root.CopyList(),
- }
- }
+ x.Tree = x.Tree.Copy()
ret.set[name] = &Template{
false,
x,
Template.Clone()
メソッド内で、x.Tree
のクローン処理がx.Tree.Copy()
を呼び出すように変更されました。これにより、parse.Tree
の完全なディープコピーが保証されます。
src/pkg/text/template/parse/parse.go
--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -30,6 +30,19 @@ type Tree struct {
vars []string // variables defined at the moment.
}
+// Copy returns a copy of the Tree. Any parsing state is discarded.
+func (t *Tree) Copy() *Tree {
+ if t == nil {
+ return nil
+ }
+ return &Tree{
+ Name: t.Name,
+ ParseName: t.ParseName,
+ Root: t.Root.CopyList(),
+ text: t.text,
+ }
+}
+
// Parse returns a map from template name to parse.Tree, created by parsing the
// templates described in the argument string. The top-level template will be
// given the specified name. If an error is encountered, parsing stops and an
Tree
構造体にCopy()
メソッドが追加されました。このメソッドは、Tree
のすべてのフィールド(Name
,ParseName
,Root
,text
)を新しいTree
インスタンスにコピーし、特にRoot
はRoot.CopyList()
を使ってディープコピーされます。
src/pkg/html/template/clone_test.go
--- a/src/pkg/html/template/clone_test.go
+++ b/src/pkg/html/template/clone_test.go
@@ -6,6 +6,8 @@ package template
import (
"bytes"
+ "errors"
+ "io/ioutil"
"testing"
"text/template/parse"
)
@@ -146,3 +148,41 @@ func TestCloneCrash(t *testing.T) {
Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`))\n \tt1.Clone()\n }\n+\n+// Ensure that this guarantee from the docs is upheld:\n+// "Further calls to Parse in the copy will add templates\n+// to the copy but not to the original."\n+func TestCloneThenParse(t *testing.T) {\n+\tt0 := Must(New("t0").Parse(`{{define "a"}}{{template "embedded"}}{{end}}`))\n+\tt1 := Must(t0.Clone())\n+\tMust(t1.Parse(`{{define "embedded"}}t1{{end}}`))\n+\tif len(t0.Templates())+1 != len(t1.Templates()) {\n+\t\tt.Error("adding a template to a clone added it to the original")\n+\t}\n+\t// double check that the embedded template isn't available in the original\n+\terr := t0.ExecuteTemplate(ioutil.Discard, "a", nil)\n+\tif err == nil {\n+\t\tt.Error("expected 'no such template' error")\n+\t}\n+}\n+\n+// https://code.google.com/p/go/issues/detail?id=5980\n+func TestFuncMapWorksAfterClone(t *testing.T) {\n+\tfuncs := FuncMap{"customFunc": func() (string, error) {\n+\t\treturn "", errors.New("issue5980")\n+\t}}\n+\n+\t// get the expected error output (no clone)\n+\tuncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))\n+\twantErr := uncloned.Execute(ioutil.Discard, nil)\n+\n+\t// toClone must be the same as uncloned. It has to be recreated from scratch,\n+\t// since cloning cannot occur after execution.\n+\ttoClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))\n+\tcloned := Must(toClone.Clone())\n+\tgotErr := cloned.Execute(ioutil.Discard, nil)\n+\n+\tif wantErr.Error() != gotErr.Error() {\n+\t\tt.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr)\n+\t}\n+}\n```
* `TestCloneThenParse` と `TestFuncMapWorksAfterClone` の2つの新しいテスト関数が追加され、クローン機能の正確性と `issue 5980` の修正が検証されます。
### `src/pkg/text/template/parse/parse_test.go`
```diff
--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -332,6 +332,22 @@ func TestIsEmpty(t *testing.T) {\n \t}\n }\n \n+func TestErrorContextWithTreeCopy(t *testing.T) {\n+\ttree, err := New("root").Parse("{{if true}}{{end}}", "", "", make(map[string]*Tree), nil)\n+\tif err != nil {\n+\t\tt.Fatalf("unexpected tree parse failure: %v", err)\n+\t}\n+\ttreeCopy := tree.Copy()\n+\twantLocation, wantContext := tree.ErrorContext(tree.Root.Nodes[0])\n+\tgotLocation, gotContext := treeCopy.ErrorContext(treeCopy.Root.Nodes[0])\n+\tif wantLocation != gotLocation {\n+\t\tt.Errorf("wrong error location want %q got %q", wantLocation, gotLocation)\n+\t}\n+\tif wantContext != gotContext {\n+\t\tt.Errorf("wrong error location want %q got %q", wantContext, gotContext)\n+\t}\n+}\n+\n // All failures, and the result is a string that must appear in the error message.\n var errorTests = []parseTest{\n \t// Check line numbers are accurate.\n```
* `TestErrorContextWithTreeCopy` が追加され、`Tree.Copy()` がエラーコンテキストを正しく保持していることを確認します。
## コアとなるコードの解説
このコミットの最も重要な変更は、`parse.Tree` 構造体に `Copy()` メソッドが追加されたことです。
```go
// Copy returns a copy of the Tree. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
if t == nil {
return nil
}
return &Tree{
Name: t.Name,
ParseName: t.ParseName,
Root: t.Root.CopyList(), // ASTのルートノードをディープコピー
text: t.text, // 元のテンプレート文字列をコピー
}
}
この Copy()
メソッドは、Tree
オブジェクトの完全な独立したコピーを作成します。特に注目すべきは以下の点です。
Root: t.Root.CopyList(),
: これは、テンプレートの抽象構文木(AST)のルートノードをディープコピーしている部分です。CopyList()
メソッドは、ノードとその子ノードを再帰的に複製し、元のASTへの参照が残らないようにします。text: t.text,
: これがこのコミットの主要な修正点です。以前はtext
フィールドが適切にコピーされていなかったため、クローンされたTree
と元のTree
が同じtext
スライスを共有していました。この行により、text
フィールドの値(テンプレートのソース文字列)が新しいTree
インスタンスにコピーされ、両者が独立して存在できるようになります。
html/template/template.go
の Clone()
メソッドでは、この新しく追加された parse.Tree.Copy()
メソッドが利用されるようになりました。
// 変更前:
// if x.Tree != nil {
// x.Tree = &parse.Tree{
// Name: x.Tree.Name,
// Root: x.Tree.Root.CopyList(),
// }
// }
// 変更後:
x.Tree = x.Tree.Copy()
この変更により、html/template
の Clone()
メソッドが呼び出された際に、内部の parse.Tree
オブジェクトが完全に独立したコピーとして生成されることが保証されます。これにより、クローンされたテンプレートに対するその後の操作(例えば、Parse
メソッドによる新しいテンプレート定義の追加)が、元のテンプレートの内部状態に影響を与えることがなくなり、issue 5980
で報告されたパニックが解消されました。
この修正は、Goのテンプレートエンジンが提供する「クローン」機能のセマンティクスを正しく実装するために不可欠であり、テンプレートを安全かつ予測可能に再利用するための基盤を強化しました。
関連リンク
- Go言語のIssueトラッカー: https://code.google.com/p/go/issues/detail?id=5980
- このコミットのGo Gerritレビューページ: https://golang.org/cl/12420044
- Go言語の公式ドキュメント:
text/template
パッケージ: https://pkg.go.dev/text/templatehtml/template
パッケージ: https://pkg.go.dev/html/template
参考にした情報源リンク
- Go言語のソースコード(上記コミットの差分)
- Go言語の公式ドキュメント
- Go言語のIssueトラッカー(
issue 5980
の内容) - Go Gerrit Code Review
- Go言語のテンプレートに関する一般的な知識