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

[インデックス 11929] ファイルの概要

このコミットは、Go言語の標準ライブラリであるhtml/templateおよびtext/templateパッケージにおけるテンプレートのクローン(複製)機能と、パース済みテンプレートツリーの追加機能に関する改善を含んでいます。特に、html/templateCloneAddParseTreeメソッドが追加され、text/templateCloneメソッドの戻り値が変更されています。

コミット

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: CloneAddParseTreeを追加。text/templateClone*Templateだけでなく(*Template, error)を返すように変更。

Fixes #2757.

変更の背景

この変更は、Go言語のIssue #2757に対応するものです。Issue #2757は、html/templateパッケージにCloneメソッドが存在しないこと、およびtext/templateCloneメソッドがエラーを返さないことに関するものでした。

ウェブアプリケーション開発において、テンプレートはしばしば共通のヘッダーやフッター、ナビゲーションなどの要素を共有しつつ、特定のページで異なる内容を表示する必要があります。このような場合、ベースとなるテンプレートを一度パースし、それを複製(クローン)して、複製したテンプレートに個別の要素を追加したり、既存の要素を上書きしたりする機能が非常に有用です。

しかし、このコミット以前のhtml/templateパッケージには、このようなテンプレートの複製機能が提供されていませんでした。また、text/templateパッケージのCloneメソッドは、複製処理中に発生しうるエラーを適切に通知するメカニズムがありませんでした。特に、テンプレートが既に実行された後にクローンしようとするなど、不正な操作が行われた場合にエラーを返すことができませんでした。

このコミットは、これらの問題を解決し、テンプレートの再利用性と堅牢性を向上させることを目的としています。具体的には、html/templateCloneAddParseTreeを追加することで、HTMLテンプレートのより柔軟な操作を可能にし、text/templateCloneメソッドにエラーハンドリングを追加することで、より安全なテンプレート操作を実現しています。

前提知識の解説

このコミットを理解するためには、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型を追加します。これにより、呼び出し元はエラーの有無をチェックし、適切に処理することができます。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. html/templateCloneメソッドを追加:

    • html/templateTemplate型にClone() (*Template, error)メソッドが追加されました。
    • このメソッドは、現在のテンプレートとその関連するすべての名前付きテンプレートを複製します。
    • 複製はディープコピーではなく、パースツリーの構造は共有されますが、名前空間は複製されます。これにより、複製されたテンプレートに対するParse呼び出しは、元のテンプレートには影響を与えずに、複製されたテンプレートの名前空間にのみテンプレートを追加します。
    • 重要な点として、テンプレートが既にExecuteされた後ではCloneはエラーを返します。これは、実行後のテンプレートの状態が変更されることを防ぎ、予測可能な動作を保証するためです。
  2. html/templateAddParseTreeメソッドを追加:

    • html/templateTemplate型にAddParseTree(name string, tree *parse.Tree) (*Template, error)メソッドが追加されました。
    • このメソッドは、指定された名前とパースツリーを持つ新しいテンプレートを作成し、現在のテンプレートに関連付けます。
    • Cloneと同様に、テンプレートが既にExecuteされた後ではAddParseTreeはエラーを返します。
  3. text/templateCloneメソッドの戻り値の変更:

    • text/templateTemplate型におけるClone()メソッドのシグネチャが*Templateから(*Template, error)に変更されました。
    • これにより、text/templateCloneメソッドも、html/templateと同様に、テンプレートが既に実行された後にクローンしようとした場合などにエラーを返すことができるようになりました。これは、より堅牢なエラーハンドリングを可能にします。
  4. 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/templateAddParseTreeを呼び出して、パースツリーを追加します。
    • 新しいTemplateオブジェクトを作成し、名前空間に登録して返します。
  • Cloneメソッドの実装: 以前は「未実装」とされていたCloneが実装されました。

    • t.nameSpace.mu.Lock()defer t.nameSpace.mu.Unlock(): 同様にミューテックスロックで保護されます。
    • if t.escaped: AddParseTreeと同様に、実行後のテンプレートのクローンは許可されず、エラーを返します。
    • textClone, err := t.text.Clone(): 内部的にtext/templateCloneを呼び出して、基盤となるテキストテンプレートをクローンします。
    • 新しい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/templateCloneメソッドが内部でtext/templateCloneを呼び出す際に、エラーハンドリングが可能になりました。
    • 現在の実装では常にnilエラーを返していますが、将来的にクローン処理でエラーが発生する可能性を考慮した変更です。

これらの変更により、html/templateはより柔軟なテンプレート操作をサポートし、text/templatehtml/templateの両方でテンプレートのクローン操作がより堅牢になりました。特に、テンプレートが一度実行されると変更ができないという制約は、テンプレートの整合性を保つ上で非常に重要です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: text/templateパッケージ
  • Go言語の公式ドキュメント: html/templateパッケージ
  • Go言語のIssueトラッカー
  • Go言語のコードレビューシステム (Gerrit)
  • Go言語のソースコード
  • クロスサイトスクリプティング (XSS) に関する一般的な情報源 (例: OWASP)