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