[インデックス 11929] ファイルの概要
このコミットは、Go言語の標準ライブラリであるhtml/templateおよびtext/templateパッケージにおけるテンプレートのクローン(複製)機能と、パース済みテンプレートツリーの追加機能に関する改善を含んでいます。特に、html/templateにCloneとAddParseTreeメソッドが追加され、text/templateのCloneメソッドの戻り値が変更されています。
コミット
commit 0c5239410e90f14dadf87d73a7d8e9161eb0bec0
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Feb 15 16:16:30 2012 +1100
html/template: add Clone and AddParseTree. Make text/template's Clone
return (*Template, error), not just *Template.
Fixes #2757.
R=r
CC=golang-dev
https://golang.org/cl/5665044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0c5239410e90f14dadf87d73a7d8e9161eb0bec0
元コミット内容
html/template: add Clone and AddParseTree. Make text/template's Clone return (*Template, error), not just *Template.
(日本語訳)
html/template: CloneとAddParseTreeを追加。text/templateのCloneを*Templateだけでなく(*Template, error)を返すように変更。
Fixes #2757.
変更の背景
この変更は、Go言語のIssue #2757に対応するものです。Issue #2757は、html/templateパッケージにCloneメソッドが存在しないこと、およびtext/templateのCloneメソッドがエラーを返さないことに関するものでした。
ウェブアプリケーション開発において、テンプレートはしばしば共通のヘッダーやフッター、ナビゲーションなどの要素を共有しつつ、特定のページで異なる内容を表示する必要があります。このような場合、ベースとなるテンプレートを一度パースし、それを複製(クローン)して、複製したテンプレートに個別の要素を追加したり、既存の要素を上書きしたりする機能が非常に有用です。
しかし、このコミット以前のhtml/templateパッケージには、このようなテンプレートの複製機能が提供されていませんでした。また、text/templateパッケージのCloneメソッドは、複製処理中に発生しうるエラーを適切に通知するメカニズムがありませんでした。特に、テンプレートが既に実行された後にクローンしようとするなど、不正な操作が行われた場合にエラーを返すことができませんでした。
このコミットは、これらの問題を解決し、テンプレートの再利用性と堅牢性を向上させることを目的としています。具体的には、html/templateにCloneとAddParseTreeを追加することで、HTMLテンプレートのより柔軟な操作を可能にし、text/templateのCloneメソッドにエラーハンドリングを追加することで、より安全なテンプレート操作を実現しています。
前提知識の解説
このコミットを理解するためには、Go言語のtext/templateおよびhtml/templateパッケージに関する基本的な知識が必要です。
-
text/templateパッケージ: Go言語の標準ライブラリで、テキストベースのテンプレートを生成するための機能を提供します。プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストファイルを定義し、データと組み合わせて最終的なテキスト出力を生成します。例えば、メールの本文や設定ファイルなどを動的に生成するのに使われます。 -
html/templateパッケージ:text/templateパッケージをベースにしており、HTML出力に特化したテンプレート機能を提供します。最も重要な特徴は、**自動エスケープ(Contextual Escaping)**機能です。これにより、テンプレートに挿入されるデータが、そのデータが使用されるHTMLのコンテキスト(例: HTML要素のテキスト、属性値、JavaScriptコード、CSSスタイルなど)に応じて自動的にエスケープされます。これは、クロスサイトスクリプティング(XSS)などのウェブセキュリティ脆弱性を防ぐ上で非常に重要です。 -
テンプレートのパース (Parse): テンプレート文字列を解析し、内部的な表現(パースツリー)に変換するプロセスです。これにより、テンプレートエンジンはテンプレートの構造とロジックを理解し、データを適用して出力を生成できます。
-
テンプレートの実行 (Execute): パースされたテンプレートにデータを与え、最終的な出力を生成するプロセスです。
-
名前付きテンプレート (Named Templates): テンプレートは名前を持つことができ、
{{define "name"}}...{{end}}構文を使って定義されます。これにより、複数のテンプレートを一つの*Templateオブジェクト内で管理し、{{template "name"}}アクションを使って他のテンプレートから参照・呼び出すことができます。 -
Cloneメソッド: 既存のテンプレートオブジェクトを複製する機能です。これにより、元のテンプレートを変更せずに、複製したテンプレートに独自の変更を加えることができます。これは、共通のベーステンプレートから派生した複数のバリエーションを作成する際に特に有用です。 -
AddParseTreeメソッド: パース済みのテンプレートツリーを既存のテンプレートオブジェクトに追加する機能です。これにより、プログラムで生成したテンプレートツリーや、別のテンプレートから抽出したツリーを、既存のテンプレートセットに組み込むことができます。 -
errorの戻り値: Go言語では、関数がエラーを返す可能性がある場合、慣習的に戻り値の最後にerror型を追加します。これにより、呼び出し元はエラーの有無をチェックし、適切に処理することができます。
技術的詳細
このコミットの主要な変更点は以下の通りです。
-
html/templateにCloneメソッドを追加:html/templateのTemplate型にClone() (*Template, error)メソッドが追加されました。- このメソッドは、現在のテンプレートとその関連するすべての名前付きテンプレートを複製します。
- 複製はディープコピーではなく、パースツリーの構造は共有されますが、名前空間は複製されます。これにより、複製されたテンプレートに対する
Parse呼び出しは、元のテンプレートには影響を与えずに、複製されたテンプレートの名前空間にのみテンプレートを追加します。 - 重要な点として、テンプレートが既に
Executeされた後ではCloneはエラーを返します。これは、実行後のテンプレートの状態が変更されることを防ぎ、予測可能な動作を保証するためです。
-
html/templateにAddParseTreeメソッドを追加:html/templateのTemplate型にAddParseTree(name string, tree *parse.Tree) (*Template, error)メソッドが追加されました。- このメソッドは、指定された名前とパースツリーを持つ新しいテンプレートを作成し、現在のテンプレートに関連付けます。
Cloneと同様に、テンプレートが既にExecuteされた後ではAddParseTreeはエラーを返します。
-
text/templateのCloneメソッドの戻り値の変更:text/templateのTemplate型におけるClone()メソッドのシグネチャが*Templateから(*Template, error)に変更されました。- これにより、
text/templateのCloneメソッドも、html/templateと同様に、テンプレートが既に実行された後にクローンしようとした場合などにエラーを返すことができるようになりました。これは、より堅牢なエラーハンドリングを可能にします。
-
lookupAndEscapeTemplate関数の変更:html/template/template.go内のlookupAndEscapeTemplate関数のシグネチャがlookupAndEscapeTemplate(wr io.Writer, name string)からlookupAndEscapeTemplate(name string)に変更されました。これは、テンプレートのルックアップとエスケープ処理が、書き込み先(io.Writer)に依存しないようにするためです。
これらの変更により、開発者はベースとなるテンプレートを一度定義し、それを複製して特定のユースケースに合わせてカスタマイズすることが容易になります。例えば、ウェブサイトの共通レイアウトを定義したテンプレートをクローンし、各ページ固有のコンテンツをそのクローンに追加するといったことが可能になります。また、エラーハンドリングが強化されたことで、テンプレート操作の信頼性が向上しています。
コアとなるコードの変更箇所
src/pkg/html/template/template.go
--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -106,14 +106,71 @@ func (t *Template) Parse(src string) (*Template, error) {
return t, nil
}
-// AddParseTree is unimplemented.
-func (t *Template) AddParseTree(name string, tree *parse.Tree) error {
- return fmt.Errorf("html/template: AddParseTree unimplemented")
+// AddParseTree creates a new template with the name and parse tree
+// and associates it with t.
+//
+// It returns an error if t has already been executed.
+func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escaped {
+ return nil, fmt.Errorf("html/template: cannot AddParseTree to %q after it has executed", t.Name())
+ }
+ text, err := t.text.AddParseTree(name, tree)
+ if err != nil {
+ return nil, err
+ }
+ ret := &Template{
+ false,
+ text,
+ t.nameSpace,
+ }
+ t.set[name] = ret
+ return ret, nil
}
-// Clone is unimplemented.
-func (t *Template) Clone(name string) error {
- return fmt.Errorf("html/template: Clone unimplemented")
+// Clone returns a duplicate of the template, including all associated
+// templates. The actual representation is not copied, but the name space of
+// associated templates is, so further calls to Parse in the copy will add
+// templates to the copy but not to the original. Clone can be used to prepare
+// common templates and use them with variant definitions for other templates
+// by adding the variants after the clone is made.
+//
+// It returns an error if t has already been executed.
+func (t *Template) Clone() (*Template, error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escaped {
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ }
+ textClone, err := t.text.Clone()
+ if err != nil {
+ return nil, err
+ }
+ ret := &Template{
+ false,
+ textClone,
+ &nameSpace{
+ set: make(map[string]*Template),
+ },
+ }
+ for _, x := range textClone.Templates() {
+ name := x.Name()
+ src := t.set[name]
+ if src == nil || src.escaped {
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ }
+ x.Tree = &parse.Tree{
+ Name: x.Tree.Name,
+ Root: x.Tree.Root.CopyList(),
+ }
+ ret.set[name] = &Template{
+ false,
+ x,
+ ret.nameSpace,
+ }
+ }
+ return ret, nil
}
// New allocates a new HTML template with the given name.
src/pkg/text/template/template.go
--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -69,9 +69,9 @@ func (t *Template) init() {
// templates. The actual representation is not copied, but the name space of
// associated templates is, so further calls to Parse in the copy will add
// templates to the copy but not to the original. Clone can be used to prepare
-// common templates and use them with variant definitions for other templates by
-// adding the variants after the clone is made.\n-func (t *Template) Clone() *Template {
+// common templates and use them with variant definitions for other templates
+// by adding the variants after the clone is made.
+func (t *Template) Clone() (*Template, error) {
nt := t.copy(nil)
nt.init()
nt.tmpl[t.name] = nt
@@ -89,7 +89,7 @@ func (t *Template) Clone() *Template {
for k, v := range t.execFuncs {
nt.execFuncs[k] = v
}
-\treturn nt
+\treturn nt, nil
}
// copy returns a shallow copy of t, with common set to the argument.
コアとなるコードの解説
html/template/template.go の変更
-
AddParseTreeメソッドの実装: 以前は「未実装」とされていたAddParseTreeが実装されました。t.nameSpace.mu.Lock()とdefer t.nameSpace.mu.Unlock(): テンプレートの名前空間に対する並行アクセスを保護するためのミューテックスロックです。if t.escaped: テンプレートが一度でも実行(Execute)されると、escapedフラグがtrueになります。実行後のテンプレートは状態が確定しているため、AddParseTreeのような構造を変更する操作は許可されず、エラーを返します。これは、テンプレートの予測可能な動作を保証し、潜在的な競合状態や予期せぬ動作を防ぐための重要な制約です。text, err := t.text.AddParseTree(name, tree): 内部的にtext/templateのAddParseTreeを呼び出して、パースツリーを追加します。- 新しい
Templateオブジェクトを作成し、名前空間に登録して返します。
-
Cloneメソッドの実装: 以前は「未実装」とされていたCloneが実装されました。t.nameSpace.mu.Lock()とdefer t.nameSpace.mu.Unlock(): 同様にミューテックスロックで保護されます。if t.escaped:AddParseTreeと同様に、実行後のテンプレートのクローンは許可されず、エラーを返します。textClone, err := t.text.Clone(): 内部的にtext/templateのCloneを呼び出して、基盤となるテキストテンプレートをクローンします。- 新しい
Templateオブジェクト(ret)を作成し、新しい名前空間(&nameSpace{set: make(map[string]*Template)})を割り当てます。これにより、クローンされたテンプレートの名前空間は元のテンプレートから独立します。 for _, x := range textClone.Templates()ループ: クローンされたテキストテンプレートに含まれるすべての名前付きテンプレートをイテレートします。src := t.set[name]: 元のテンプレートの名前空間から対応するテンプレートを取得します。if src == nil || src.escaped: 元のテンプレートのサブテンプレートがnilであるか、または既に実行されている場合もエラーを返します。これは、クローン操作の一貫性を保つためです。x.Tree = &parse.Tree{... Root: x.Tree.Root.CopyList()}: ここが重要なポイントで、パースツリーのルートノードをCopyList()でディープコピーしています。これにより、クローンされたテンプレートのパースツリーに対する変更が、元のテンプレートのパースツリーに影響を与えないようにします。ret.set[name] = &Template{...}: クローンされたサブテンプレートを新しい名前空間に登録します。
text/template/template.go の変更
Cloneメソッドのシグネチャ変更:func (t *Template) Clone() *Templateからfunc (t *Template) Clone() (*Template, error)に変更されました。- これにより、
Cloneメソッドはエラーを返すことができるようになり、html/templateのCloneメソッドが内部でtext/templateのCloneを呼び出す際に、エラーハンドリングが可能になりました。 - 現在の実装では常に
nilエラーを返していますが、将来的にクローン処理でエラーが発生する可能性を考慮した変更です。
- これにより、
これらの変更により、html/templateはより柔軟なテンプレート操作をサポートし、text/templateとhtml/templateの両方でテンプレートのクローン操作がより堅牢になりました。特に、テンプレートが一度実行されると変更ができないという制約は、テンプレートの整合性を保つ上で非常に重要です。
関連リンク
- Go Issue #2757: https://github.com/golang/go/issues/2757
- Go CL 5665044: https://golang.org/cl/5665044
参考にした情報源リンク
- Go言語の公式ドキュメント:
text/templateパッケージ - Go言語の公式ドキュメント:
html/templateパッケージ - Go言語のIssueトラッカー
- Go言語のコードレビューシステム (Gerrit)
- Go言語のソースコード
- クロスサイトスクリプティング (XSS) に関する一般的な情報源 (例: OWASP)