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

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

このコミットは、Go言語の標準ライブラリであるtext/templateおよびhtml/templateパッケージにおける、テンプレートの追加方法に関するAPIの変更を扱っています。具体的には、既存のAddメソッドをAddParseTreeメソッドに置き換えることで、テンプレートのパースツリー(解析済み構造)を直接追加し、それに基づいて新しいテンプレートを作成するという操作の意図をより明確にしています。この変更は、APIの利用者がテンプレートの内部的な動作をより直感的に理解し、安全かつ効率的にテンプレートを管理できるようにすることを目的としています。

コミット

  • コミットハッシュ: d38cc47c0c2d830fd745b49bf6be1b0ff0e17b14
  • 作者: Rob Pike (r@golang.org)
  • 日付: 2011年12月1日 木曜日 09:19:53 -0800
  • コミットメッセージ:
    text/template: replace Add with AddParseTree
    Makes it clear we're adding exactly one tree and creating a
    new template for it.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5448077
    

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d38cc47c0c2d830fd745b49bf6be1b0ff0e17b14

元コミット内容

text/template: replace Add with AddParseTree
Makes it clear we're adding exactly one tree and creating a
new template for it.

R=rsc
CC=golang-dev
https://golang.org/cl/5448077

変更の背景

Go言語のテンプレートパッケージ(text/templateおよびhtml/template)は、ウェブアプリケーションやCLIツールなどで動的なコンテンツを生成するために広く利用されています。これらのパッケージでは、複数の名前付きテンプレートを一つの*Templateオブジェクト(テンプレートセット)にまとめる機能があり、{{template "name"}}のような構文を使って、あるテンプレートから別のテンプレートを呼び出すことが可能です。

このコミット以前は、テンプレートセットに別のテンプレートを追加するためにAddというメソッドが提供されていました。しかし、このAddメソッドは引数として別の*Templateオブジェクトを受け取っていました。この設計にはいくつかの課題がありました。

  1. 意図の不明瞭さ: Add(*Template)というシグネチャだけでは、このメソッドが「既存のテンプレートオブジェクトを単にセットに追加する」のか、「渡されたテンプレートオブジェクトのパースツリーを抽出し、それに基づいて新しいテンプレートをセット内に作成する」のかが直感的に分かりにくいという問題がありました。特に、テンプレートの内部表現であるパースツリーの概念を考慮すると、この曖昧さは開発者の混乱を招く可能性がありました。
  2. 柔軟性の欠如: *Templateオブジェクト全体を渡す必要があるため、既にパース済みのparse.Treeを直接追加したい場合など、より低レベルな操作を行いたい場合に不便でした。

これらの背景から、Rob Pike氏はこのAPIをより明確で、かつ意図が伝わりやすいものに改善する必要があると判断しました。その結果、AddメソッドをAddParseTreeに置き換え、引数としてテンプレート名とparse.Treeを直接受け取るように変更されました。これにより、「単一のパースツリーを追加し、それに基づいて新しいテンプレートを作成する」という操作がAPIレベルで明確に表現されるようになりました。

前提知識の解説

このコミットの変更内容を深く理解するためには、以下のGo言語の概念とパッケージに関する知識が不可欠です。

  • Go言語のtext/templateパッケージ: Goの標準ライブラリに含まれる、テキストベースのテンプレートエンジンです。ファイルや文字列からテンプレートを読み込み、プレースホルダー(例: {{.Name}})や制御構造(例: {{if .Condition}}...{{end}}, {{range .Items}}...{{end}})を定義できます。これらのテンプレートは、Goのデータ構造(構造体、マップ、スライスなど)と組み合わせて実行され、最終的なテキスト出力を生成します。例えば、HTML、XML、JSON、設定ファイル、ソースコードなどを動的に生成するのに利用されます。

  • Go言語のhtml/templateパッケージ: text/templateパッケージを基盤として構築されていますが、HTML出力を安全に生成することに特化しています。最も重要な機能は、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ処理です。テンプレート内でユーザーが入力したデータや信頼できないソースからのデータがHTMLとして出力される際、html/templateは自動的に特殊文字(<, >, &, ', "など)をHTMLエンティティに変換し、悪意のあるスクリプトの実行を防ぎます。ウェブアプリケーション開発において、ユーザーからの入力を表示する際には、セキュリティ上の理由からhtml/templateの使用が強く推奨されます。

  • テンプレートのパースツリー (text/template/parse.Tree): text/templateおよびhtml/templateパッケージがテンプレート文字列を処理する際、まずその文字列を解析(パース)します。この解析プロセスによって、テンプレートの構造がメモリ上にツリー状のデータ構造として表現されます。このツリーがtext/template/parseパッケージで定義されているTree型です。Treeは、テンプレート内の各要素(テキスト、アクション、パイプライン、コマンドなど)をノードとして持ち、それらの関係性を階層的に表現します。テンプレートの実行時には、このパースツリーがトラバースされ、データが適用されて最終的な出力が生成されます。parse.Treeを直接操作することは、テンプレートエンジンの低レベルな動作を理解したり、カスタムのテンプレート処理を実装したりする際に役立ちます。

  • テンプレートの関連付けと名前空間: Goのテンプレートエンジンでは、複数の独立したテンプレートを一つの*Templateインスタンス(テンプレートセット)にまとめることができます。各テンプレートは一意の名前を持ち、この名前を使ってテンプレートセット内で識別されます。これにより、{{template "header"}}のように、あるテンプレートから別の名前付きテンプレートを呼び出すことが可能になります。これは、共通のヘッダーやフッター、サイドバーなどのコンポーネントを再利用する際に非常に便利です。AddAddParseTreeのようなメソッドは、このテンプレートセットに新しい名前付きテンプレートを追加し、それらを関連付ける役割を担います。これにより、テンプレート間の依存関係を管理し、モジュール化されたテンプレート設計を促進します。

技術的詳細

このコミットにおける技術的な変更は、主にtext/templateパッケージのAPI変更と、それに伴うhtml/templateパッケージの調整、そして新しいAPIの動作を検証するためのテストケースの追加に集約されます。

  1. text/template.Template.AddからAddParseTreeへの変更:

    • 旧APIシグネチャ: func (t *Template) Add(arg *Template) error
      • このメソッドは、既存の*Templateオブジェクトを引数として受け取り、それを現在のテンプレートセットに追加しようとしました。しかし、その内部的な動作(パースツリーの扱い)が不明瞭でした。
    • 新APIシグネチャ: func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)
      • 新しいAddParseTreeメソッドは、テンプレートのname(文字列)と、既にパースされた*parse.Treeオブジェクトを直接引数として受け取ります。
      • 戻り値は、追加された新しい*Templateオブジェクトとエラーです。これにより、メソッドチェーンでさらに操作を続けたり、追加されたテンプレートを直接利用したりすることが可能になります。
      • この変更により、APIの呼び出し元は「特定の名前で、このパースツリーを持つ新しいテンプレートを追加する」という明確な意図を持って操作できるようになりました。
  2. html/templateにおける変更の追従:

    • html/templateパッケージはtext/templateパッケージを基盤としているため、text/templateのAPI変更に追従する必要があります。
    • src/pkg/html/template/template.goでは、AddメソッドがAddParseTreeにリネームされ、そのシグネチャがtext/templateの新しいシグネチャに合わせて更新されました。ただし、html/templateAddParseTreeは引き続き「未実装」としてエラーを返します。これは、html/templateがテンプレートの追加に関して独自のセキュリティ要件や処理ロジックを持つため、直接的なparse.Treeの追加を許可しない設計になっていることを示唆しています。
    • src/pkg/html/template/escape.goでは、内部的にtext/templateの機能を利用している箇所(e.tmpl.text.Add(t))が、新しいe.tmpl.text.AddParseTree(t.Name(), t.Tree)に置き換えられました。AddParseTreeがエラーを返す可能性があるため、if _, err := ...; err != nilという形式でエラーハンドリングが追加されています。これは、html/templatetext/templateの変更に適切に対応し、互換性を維持していることを示しています。
  3. 新しいテストケースの追加:

    • src/pkg/text/template/multi_test.goTestAddParseTreeという新しいテスト関数が追加されました。
    • このテストは、AddParseTreeメソッドが期待通りに動作することを確認するためのものです。具体的には、既存のテンプレートセットに新しいパースツリーを基にしたテンプレートを追加し、そのテンプレートが正しく実行され、期待される出力が生成されることを検証しています。
    • テストコードでは、まず既存のテンプレートをパースし、次にparse.Parse関数を使って新しいテンプレート文字列からparse.Treeを生成しています。その後、root.AddParseTree("c", tree["c"])を呼び出して、この新しいパースツリーを既存のテンプレートセットに追加しています。最後に、追加されたテンプレートを含むセットを実行し、結果を検証しています。これは、API変更に伴う機能保証の重要な側面であり、リグレッションを防ぐためのベストプラクティスです。
  4. text/template.Template.Parseのドキュメント更新:

    • src/pkg/html/template/template.goParseメソッドのコメントが更新され、ネストされたテンプレート定義の扱い、および非空のテンプレートが同じ名前の非空のテンプレートを置き換える場合の挙動について、より詳細な説明が追加されました。これは、テンプレートのパースと定義に関する理解を深めるための重要な改善です。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/pkg/html/template/escape.go:

    --- a/src/pkg/html/template/escape.go
    +++ b/src/pkg/html/template/escape.go
    @@ -720,7 +720,9 @@ func (e *escaper) commit() {
     		e.template(name).Funcs(funcMap)
     	}
     	for _, t := range e.derived {
    -		e.tmpl.text.Add(t)
    +		if _, err := e.tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil {
    +			panic("error adding derived template")
    +		}
     	}
     	for n, s := range e.actionNodeEdits {
     		ensurePipelineContains(n.Pipe, s)
    
    • e.tmpl.text.Add(t)という古いAPI呼び出しが、新しいe.tmpl.text.AddParseTree(t.Name(), t.Tree)に置き換えられました。AddParseTreeがエラーを返す可能性があるため、エラーチェックとパニック処理が追加されています。
  • src/pkg/html/template/template.go:

    --- a/src/pkg/html/template/template.go
    +++ b/src/pkg/html/template/template.go
    @@ -11,6 +11,7 @@ import (
     	"path/filepath"
     	"sync"
     	"text/template"
    +"text/template/parse"
     )
    
     // Template is a specialized template.Template that produces a safe HTML
    @@ -94,9 +99,9 @@ func (t *Template) Parse(src string) (*Template, error) {
     	return t, nil
     }
    
    -// Add is unimplemented.
    -func (t *Template) Add(*Template) error {
    -	return fmt.Errorf("html/template: Add unimplemented")
    +// AddParseTree is unimplemented.
    +func (t *Template) AddParseTree(name string, tree *parse.Tree) error {
    +	return fmt.Errorf("html/template: AddParseTree unimplemented")
     }
    
     // Clone is unimplemented.
    
    • text/template/parseパッケージが新しくインポートされました。
    • AddメソッドがAddParseTreeにリネームされ、そのシグネチャがfunc (t *Template) AddParseTree(name string, tree *parse.Tree) errorに変更されました。実装は引き続き「未実装」としてエラーを返します。
    • Parseメソッドのドキュメンテーションが更新され、テンプレートのパースに関する詳細な説明が追加されました。
  • src/pkg/text/template/multi_test.go:

    --- a/src/pkg/text/template/multi_test.go
    +++ b/src/pkg/text/template/multi_test.go
    @@ -10,6 +10,7 @@ import (
     	"bytes"
     	"fmt"
     	"testing"
    +"text/template/parse"
     )
    
     type isEmptyTest struct {
    @@ -258,3 +259,30 @@ func TestClone(t *testing.T) {
     		t.Errorf("expected %q got %q", "bclone", b.String())
     	}\n}\n+\n+func TestAddParseTree(t *testing.T) {\n+\t// Create some templates.\n+\troot, err := New("root").Parse(cloneText1)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\t_, err = root.Parse(cloneText2)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\t// Add a new parse tree.\n+\ttree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tadded, err := root.AddParseTree("c", tree["c"])\n+\t// Execute.\n+\tvar b bytes.Buffer\n+\terr = added.ExecuteTemplate(&b, "a", 0)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif b.String() != "broot" {\n+\t\tt.Errorf("expected %q got %q", "broot", b.String())\n+\t}\n+}\n    ```
    - `text/template/parse`パッケージがインポートされました。
    - `TestAddParseTree`という新しいテスト関数が追加され、`text/template.Template.AddParseTree`メソッドの動作を検証しています。
    
    
  • src/pkg/text/template/template.go:

    --- a/src/pkg/text/template/template.go
    +++ b/src/pkg/text/template/template.go
    @@ -103,21 +103,16 @@ func (t *Template) copy(c *common) *Template {
     	return nt
     }
    
    -// Add associates the argument template, arg, with t, and vice versa,
    -// so they may invoke each other. To do this, it also removes any
    -// prior associations arg may have. Except for losing the link to
    -// arg, templates associated with arg are otherwise unaffected. It
    -// is an error if the argument template's name is already associated
    -// with t.  Add is here to support html/template and is not intended
    -// for other uses.
    -// TODO: make this take a parse.Tree argument instead of a template.
    -func (t *Template) Add(arg *Template) error {
    -	if t.tmpl[arg.name] != nil {\n-\t\treturn fmt.Errorf("template: redefinition of template %q", arg.name)\n+func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
    +	if t.tmpl[name] != nil {
    +		return nil, fmt.Errorf("template: redefinition of template %q", name)
     	}
    -\targ.common = t.common
    -\tt.tmpl[arg.name] = arg
    -\treturn nil
    +\tnt := t.New(name)
    +\tnt.Tree = tree
    +\tt.tmpl[name] = nt
    +\treturn nt, nil
     }
    
     // Templates returns a slice of the templates associated with t, including t
    
    • 古いAddメソッドが完全に削除され、新しいAddParseTreeメソッドが追加されました。
    • AddParseTreeの実装は、指定された名前のテンプレートが既に存在しないかを確認し、新しい*Templateインスタンスを作成し、そのTreeフィールドに渡されたparse.Treeを割り当て、最後に現在のテンプレートセットに新しいテンプレートを登録しています。

コアとなるコードの解説

このコミットの核心は、src/pkg/text/template/template.goにおけるAddParseTreeメソッドの新しい実装です。

func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
	// 1. テンプレート名の重複チェック
	if t.tmpl[name] != nil {
		return nil, fmt.Errorf("template: redefinition of template %q", name)
	}

	// 2. 新しいテンプレートインスタンスの作成
	// t.New(name) は、現在のテンプレートセット (t) に関連付けられた
	// 新しい空のテンプレートインスタンスを返します。
	nt := t.New(name)

	// 3. パースツリーの割り当て
	// 引数として渡された parse.Tree を、新しく作成したテンプレートインスタンスの
	// Tree フィールドに直接割り当てます。これにより、このテンプレートが持つべき
	// 解析済みの構造が設定されます。
	nt.Tree = tree

	// 4. テンプレートセットへの登録
	// 新しく作成されたテンプレート nt を、現在のテンプレートセット t の
	// 内部マップ (t.tmpl) に、指定された name で登録します。
	// これにより、このテンプレートがセットの一部として認識され、
	// 他のテンプレートから参照できるようになります。
	t.tmpl[name] = nt

	// 5. 新しいテンプレートインスタンスの返却
	// 呼び出し元は、追加されたテンプレートを直接操作したり、
	// さらにメソッドチェーンで処理を続けたりすることが可能になります。
	return nt, nil
}

この新しいAddParseTreeメソッドは、以下の点で以前のAddメソッドよりも優れています。

  • 明確な意図: メソッド名がAddParseTreeであるため、開発者は「パースツリーを追加する」という操作が直接行われることを明確に理解できます。
  • 直接的なパースツリーの利用: *parse.Treeを直接引数として受け取ることで、テンプレート文字列をパースするプロセスと、その結果得られたパースツリーをテンプレートセットに追加するプロセスを分離できます。これにより、より柔軟なテンプレート管理が可能になります。例えば、テンプレート文字列を動的に生成し、それをパースしてから追加するといった高度なシナリオに対応しやすくなります。
  • 新しいテンプレートの返却: 追加された新しい*Templateインスタンスを返すことで、呼び出し元はそのインスタンスに対してさらにメソッドを呼び出す(例: Funcsで関数マップを追加する)といったチェーン操作が可能になり、APIの使い勝手が向上します。

html/template/escape.goでの変更は、html/templateが内部的にtext/templateの機能を利用しているため、そのAPI変更に追従したものです。AddParseTreeの戻り値が(*Template, error)になったため、if _, err := ...; err != nilという形式でエラーチェックが追加されています。これは、html/templatetext/templateの変更に適切に対応し、互換性を維持していることを示しています。

関連リンク

参考にした情報源リンク

  • Go Gerrit Change-ID: https://golang.org/cl/5448077 (このコミットに関するGoのコードレビューシステムGerritでの議論や詳細な変更履歴が確認できます。)
  • Go言語のテンプレートに関する一般的なドキュメントやチュートリアル(Goのテンプレートエンジンの基本的な概念や使用方法を理解するために参照しました。)

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

このコミットは、Go言語の標準ライブラリであるtext/templateおよびhtml/templateパッケージにおける、テンプレートの追加方法に関するAPIの変更を扱っています。具体的には、既存のAddメソッドをAddParseTreeメソッドに置き換えることで、テンプレートのパースツリー(解析済み構造)を直接追加し、それに基づいて新しいテンプレートを作成するという操作の意図をより明確にしています。この変更は、APIの利用者がテンプレートの内部的な動作をより直感的に理解し、安全かつ効率的にテンプレートを管理できるようにすることを目的としています。

コミット

  • コミットハッシュ: d38cc47c0c2d830fd745b49bf6be1b0ff0e17b14
  • 作者: Rob Pike (r@golang.org)
  • 日付: 2011年12月1日 木曜日 09:19:53 -0800
  • コミットメッセージ:
    text/template: replace Add with AddParseTree
    Makes it clear we're adding exactly one tree and creating a
    new template for it.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5448077
    

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d38cc47c0c2d830fd745b49bf6be1b0ff0e17b14

元コミット内容

text/template: replace Add with AddParseTree
Makes it clear we're adding exactly one tree and creating a
new template for it.

R=rsc
CC=golang-dev
https://golang.org/cl/5448077

変更の背景

Go言語のテンプレートパッケージ(text/templateおよびhtml/template)は、ウェブアプリケーションやCLIツールなどで動的なコンテンツを生成するために広く利用されています。これらのパッケージでは、複数の名前付きテンプレートを一つの*Templateオブジェクト(テンプレートセット)にまとめる機能があり、{{template "name"}}のような構文を使って、あるテンプレートから別のテンプレートを呼び出すことが可能です。

このコミット以前は、テンプレートセットに別のテンプレートを追加するためにAddというメソッドが提供されていました。しかし、このAddメソッドは引数として別の*Templateオブジェクトを受け取っていました。この設計にはいくつかの課題がありました。

  1. 意図の不明瞭さ: Add(*Template)というシグネチャだけでは、このメソッドが「既存のテンプレートオブジェクトを単にセットに追加する」のか、「渡されたテンプレートオブジェクトのパースツリーを抽出し、それに基づいて新しいテンプレートをセット内に作成する」のかが直感的に分かりにくいという問題がありました。特に、テンプレートの内部表現であるパースツリーの概念を考慮すると、この曖昧さは開発者の混乱を招く可能性がありました。
  2. 柔軟性の欠如: *Templateオブジェクト全体を渡す必要があるため、既にパース済みのparse.Treeを直接追加したい場合など、より低レベルな操作を行いたい場合に不便でした。

これらの背景から、Rob Pike氏はこのAPIをより明確で、かつ意図が伝わりやすいものに改善する必要があると判断しました。その結果、AddメソッドをAddParseTreeに置き換え、引数としてテンプレート名とparse.Treeを直接受け取るように変更されました。これにより、「単一のパースツリーを追加し、それに基づいて新しいテンプレートを作成する」という操作がAPIレベルで明確に表現されるようになりました。

前提知識の解説

このコミットの変更内容を深く理解するためには、以下のGo言語の概念とパッケージに関する知識が不可欠です。

  • Go言語のtext/templateパッケージ: Goの標準ライブラリに含まれる、テキストベースのテンプレートエンジンです。ファイルや文字列からテンプレートを読み込み、プレースホルダー(例: {{.Name}})や制御構造(例: {{if .Condition}}...{{end}}, {{range .Items}}...{{end}})を定義できます。これらのテンプレートは、Goのデータ構造(構造体、マップ、スライスなど)と組み合わせて実行され、最終的なテキスト出力を生成します。例えば、HTML、XML、JSON、設定ファイル、ソースコードなどを動的に生成するのに利用されます。

  • Go言語のhtml/templateパッケージ: text/templateパッケージを基盤として構築されていますが、HTML出力を安全に生成することに特化しています。最も重要な機能は、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ処理です。テンプレート内でユーザーが入力したデータや信頼できないソースからのデータがHTMLとして出力される際、html/templateは自動的に特殊文字(<, >, &, ', "など)をHTMLエンティティに変換し、悪意のあるスクリプトの実行を防ぎます。ウェブアプリケーション開発において、ユーザーからの入力を表示する際には、セキュリティ上の理由からhtml/templateの使用が強く推奨されます。

  • テンプレートのパースツリー (text/template/parse.Tree): text/templateおよびhtml/templateパッケージがテンプレート文字列を処理する際、まずその文字列を解析(パース)します。この解析プロセスによって、テンプレートの構造がメモリ上にツリー状のデータ構造として表現されます。このツリーがtext/template/parseパッケージで定義されているTree型です。Treeは、テンプレート内の各要素(テキスト、アクション、パイプライン、コマンドなど)をノードとして持ち、それらの関係性を階層的に表現します。テンプレートの実行時には、このパースツリーがトラバースされ、データが適用されて最終的な出力が生成されます。parse.Treeを直接操作することは、テンプレートエンジンの低レベルな動作を理解したり、カスタムのテンプレート処理を実装したりする際に役立ちます。

  • テンプレートの関連付けと名前空間: Goのテンプレートエンジンでは、複数の独立したテンプレートを一つの*Templateインスタンス(テンプレートセット)にまとめることができます。各テンプレートは一意の名前を持ち、この名前を使ってテンプレートセット内で識別されます。これにより、{{template "header"}}のように、あるテンプレートから別の名前付きテンプレートを呼び出すことが可能になります。これは、共通のヘッダーやフッター、サイドバーなどのコンポーネントを再利用する際に非常に便利です。AddAddParseTreeのようなメソッドは、このテンプレートセットに新しい名前付きテンプレートを追加し、それらを関連付ける役割を担います。これにより、テンプレート間の依存関係を管理し、モジュール化されたテンプレート設計を促進します。

技術的詳細

このコミットにおける技術的な変更は、主にtext/templateパッケージのAPI変更と、それに伴うhtml/templateパッケージの調整、そして新しいAPIの動作を検証するためのテストケースの追加に集約されます。

  1. text/template.Template.AddからAddParseTreeへの変更:

    • 旧APIシグネチャ: func (t *Template) Add(arg *Template) error
      • このメソッドは、既存の*Templateオブジェクトを引数として受け取り、それを現在のテンプレートセットに追加しようとしました。しかし、その内部的な動作(パースツリーの扱い)が不明瞭でした。
    • 新APIシグネチャ: func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)
      • 新しいAddParseTreeメソッドは、テンプレートのname(文字列)と、既にパースされた*parse.Treeオブジェクトを直接引数として受け取ります。
      • 戻り値は、追加された新しい*Templateオブジェクトとエラーです。これにより、メソッドチェーンでさらに操作を続けたり、追加されたテンプレートを直接利用したりすることが可能になります。
      • この変更により、APIの呼び出し元は「特定の名前で、このパースツリーを持つ新しいテンプレートを追加する」という明確な意図を持って操作できるようになりました。
  2. html/templateにおける変更の追従:

    • html/templateパッケージはtext/templateパッケージを基盤としているため、text/templateのAPI変更に追従する必要があります。
    • src/pkg/html/template/template.goでは、AddメソッドがAddParseTreeにリネームされ、そのシグネチャがtext/templateの新しいシグネチャに合わせて更新されました。ただし、html/templateAddParseTreeは引き続き「未実装」としてエラーを返します。これは、html/templateがテンプレートの追加に関して独自のセキュリティ要件や処理ロジックを持つため、直接的なparse.Treeの追加を許可しない設計になっていることを示唆しています。
    • src/pkg/html/template/escape.goでは、内部的にtext/templateの機能を利用している箇所(e.tmpl.text.Add(t))が、新しいe.tmpl.text.AddParseTree(t.Name(), t.Tree)に置き換えられました。AddParseTreeがエラーを返す可能性があるため、if _, err := ...; err != nilという形式でエラーハンドリングが追加されています。これは、html/templatetext/templateの変更に適切に対応し、互換性を維持していることを示しています。
  3. 新しいテストケースの追加:

    • src/pkg/text/template/multi_test.goTestAddParseTreeという新しいテスト関数が追加されました。
    • このテストは、AddParseTreeメソッドが期待通りに動作することを確認するためのものです。具体的には、既存のテンプレートセットに新しいパースツリーを基にしたテンプレートを追加し、そのテンプレートが正しく実行され、期待される出力が生成されることを検証しています。
    • テストコードでは、まず既存のテンプレートをパースし、次にparse.Parse関数を使って新しいテンプレート文字列からparse.Treeを生成しています。その後、root.AddParseTree("c", tree["c"])を呼び出して、この新しいパースツリーを既存のテンプレートセットに追加しています。最後に、追加されたテンプレートを含むセットを実行し、結果を検証しています。これは、API変更に伴う機能保証の重要な側面であり、リグレッションを防ぐためのベストプラクティスです。
  4. text/template.Template.Parseのドキュメント更新:

    • src/pkg/html/template/template.goParseメソッドのコメントが更新され、ネストされたテンプレート定義の扱い、および非空のテンプレートが同じ名前の非空のテンプレートを置き換える場合の挙動について、より詳細な説明が追加されました。これは、テンプレートのパースと定義に関する理解を深めるための重要な改善です。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/pkg/html/template/escape.go:

    --- a/src/pkg/html/template/escape.go
    +++ b/src/pkg/html/template/escape.go
    @@ -720,7 +720,9 @@ func (e *escaper) commit() {
     		e.template(name).Funcs(funcMap)
     	}
     	for _, t := range e.derived {
    -		e.tmpl.text.Add(t)
    +		if _, err := e.tmpl.text.AddParseTree(t.Name(), t.Tree); err != nil {
    +			panic("error adding derived template")
    +		}
     	}
     	for n, s := range e.actionNodeEdits {
     		ensurePipelineContains(n.Pipe, s)
    
    • e.tmpl.text.Add(t)という古いAPI呼び出しが、新しいe.tmpl.text.AddParseTree(t.Name(), t.Tree)に置き換えられました。AddParseTreeがエラーを返す可能性があるため、エラーチェックとパニック処理が追加されています。
  • src/pkg/html/template/template.go:

    --- a/src/pkg/html/template/template.go
    +++ b/src/pkg/html/template/template.go
    @@ -11,6 +11,7 @@ import (
     	"path/filepath"
     	"sync"
     	"text/template"
    +"text/template/parse"
     )
    
     // Template is a specialized template.Template that produces a safe HTML
    @@ -94,9 +99,9 @@ func (t *Template) Parse(src string) (*Template, error) {
     	return t, nil
     }
    
    -// Add is unimplemented.
    -func (t *Template) Add(*Template) error {
    -	return fmt.Errorf("html/template: Add unimplemented")
    +// AddParseTree is unimplemented.
    +func (t *Template) AddParseTree(name string, tree *parse.Tree) error {
    +	return fmt.Errorf("html/template: AddParseTree unimplemented")
     }
    
     // Clone is unimplemented.
    
    • text/template/parseパッケージが新しくインポートされました。
    • AddメソッドがAddParseTreeにリネームされ、そのシグネチャがfunc (t *Template) AddParseTree(name string, tree *parse.Tree) errorに変更されました。実装は引き続き「未実装」としてエラーを返します。
    • Parseメソッドのドキュメンテーションが更新され、テンプレートのパースに関する詳細な説明が追加されました。
  • src/pkg/text/template/multi_test.go:

    --- a/src/pkg/text/template/multi_test.go
    +++ b/src/pkg/text/template/multi_test.go
    @@ -10,6 +10,7 @@ import (
     	"bytes"
     	"fmt"
     	"testing"
    +"text/template/parse"
     )
    
     type isEmptyTest struct {
    @@ -258,3 +259,30 @@ func TestClone(t *testing.T) {
     		t.Errorf("expected %q got %q", "bclone", b.String())
     	}\n}\n+\n+func TestAddParseTree(t *testing.T) {\n+\t// Create some templates.\n+\troot, err := New("root").Parse(cloneText1)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\t_, err = root.Parse(cloneText2)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\t// Add a new parse tree.\n+\ttree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tadded, err := root.AddParseTree("c", tree["c"])\n+\t// Execute.\n+\tvar b bytes.Buffer\n+\terr = added.ExecuteTemplate(&b, "a", 0)\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif b.String() != "broot" {\n+\t\tt.Errorf("expected %q got %q", "broot", b.String())\n+\t}\n+}\n    ```
    - `text/template/parse`パッケージがインポートされました。
    - `TestAddParseTree`という新しいテスト関数が追加され、`text/template.Template.AddParseTree`メソッドの動作を検証しています。
    
    
  • src/pkg/text/template/template.go:

    --- a/src/pkg/text/template/template.go
    +++ b/src/pkg/text/template/template.go
    @@ -103,21 +103,16 @@ func (t *Template) copy(c *common) *Template {
     	return nt
     }
    
    -// Add associates the argument template, arg, with t, and vice versa,
    -// so they may invoke each other. To do this, it also removes any
    -// prior associations arg may have. Except for losing the link to
    -// arg, templates associated with arg are otherwise unaffected. It
    -// is an error if the argument template's name is already associated
    -// with t.  Add is here to support html/template and is not intended
    -// for other uses.
    -// TODO: make this take a parse.Tree argument instead of a template.
    -func (t *Template) Add(arg *Template) error {
    -	if t.tmpl[arg.name] != nil {\n-\t\treturn fmt.Errorf("template: redefinition of template %q", arg.name)\n+func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
    +	if t.tmpl[name] != nil {
    +		return nil, fmt.Errorf("template: redefinition of template %q", name)
     	}
    -\targ.common = t.common
    -\tt.tmpl[arg.name] = arg
    -\treturn nil
    +\tnt := t.New(name)
    +\tnt.Tree = tree
    +\tt.tmpl[name] = nt
    +\treturn nt, nil
     }
    
     // Templates returns a slice of the templates associated with t, including t
    
    • 古いAddメソッドが完全に削除され、新しいAddParseTreeメソッドが追加されました。
    • AddParseTreeの実装は、指定された名前のテンプレートが既に存在しないかを確認し、新しい*Templateインスタンスを作成し、そのTreeフィールドに渡されたparse.Treeを割り当て、最後に現在のテンプレートセットに新しいテンプレートを登録しています。

コアとなるコードの解説

このコミットの核心は、src/pkg/text/template/template.goにおけるAddParseTreeメソッドの新しい実装です。

func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
	// 1. テンプレート名の重複チェック
	if t.tmpl[name] != nil {
		return nil, fmt.Errorf("template: redefinition of template %q", name)
	}

	// 2. 新しいテンプレートインスタンスの作成
	// t.New(name) は、現在のテンプレートセット (t) に関連付けられた
	// 新しい空のテンプレートインスタンスを返します。
	nt := t.New(name)

	// 3. パースツリーの割り当て
	// 引数として渡された parse.Tree を、新しく作成したテンプレートインスタンスの
	// Tree フィールドに直接割り当てます。これにより、このテンプレートが持つべき
	// 解析済みの構造が設定されます。
	nt.Tree = tree

	// 4. テンプレートセットへの登録
	// 新しく作成されたテンプレート nt を、現在のテンプレートセット t の
	// 内部マップ (t.tmpl) に、指定された name で登録します。
	// これにより、このテンプレートがセットの一部として認識され、
	// 他のテンプレートから参照できるようになります。
	t.tmpl[name] = nt

	// 5. 新しいテンプレートインスタンスの返却
	// 呼び出し元は、追加されたテンプレートを直接操作したり、
	// さらにメソッドチェーンで処理を続けたりすることが可能になります。
	return nt, nil
}

この新しいAddParseTreeメソッドは、以下の点で以前のAddメソッドよりも優れています。

  • 明確な意図: メソッド名がAddParseTreeであるため、開発者は「パースツリーを追加する」という操作が直接行われることを明確に理解できます。
  • 直接的なパースツリーの利用: *parse.Treeを直接引数として受け取ることで、テンプレート文字列をパースするプロセスと、その結果得られたパースツリーをテンプレートセットに追加するプロセスを分離できます。これにより、より柔軟なテンプレート管理が可能になります。例えば、テンプレート文字列を動的に生成し、それをパースしてから追加するといった高度なシナリオに対応しやすくなります。
  • 新しいテンプレートの返却: 追加された新しい*Templateインスタンスを返すことで、呼び出し元はそのインスタンスに対してさらにメソッドを呼び出す(例: Funcsで関数マップを追加する)といったチェーン操作が可能になり、APIの使い勝手が向上します。

html/template/escape.goでの変更は、html/templateが内部的にtext/templateの機能を利用しているため、そのAPI変更に追従したものです。AddParseTreeの戻り値が(*Template, error)になったため、if _, err := ...; err != nilという形式でエラーチェックが追加されています。これは、html/templatetext/templateの変更に適切に対応し、互換性を維持していることを示しています。

関連リンク

参考にした情報源リンク

  • Go Gerrit Change-ID: https://golang.org/cl/5448077 (このコミットに関するGoのコードレビューシステムGerritでの議論や詳細な変更履歴が確認できます。)
  • Go言語のテンプレートに関する一般的なドキュメントやチュートリアル(Goのテンプレートエンジンの基本的な概念や使用方法を理解するために参照しました。)