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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおけるいくつかの重要な修正と機能追加を扱っています。特に、html/template パッケージとの連携を改善し、テンプレートの初期化状態やエラーハンドリングに関する問題を解決することを目的としています。

コミット

commit 5f6027e9ad9a6f115399a93c5d330cbf2d66e85f
Author: Rob Pike <r@golang.org>
Date:   Mon Nov 28 10:42:57 2011 -0800

    text/template: address a couple of issues for html/template
    - allow Lookup to work on uninitialized templates
    - fix bug in add: can't error after parser is stopped
    - add Add method for html/template
    
    R=adg, rogpeppe, r, rsc
    CC=golang-dev
    https://golang.org/cl/5436080

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

https://github.com/golang/go/commit/5f6027e9ad9a6f115399a93c5d330cbf2d66e85f

元コミット内容

このコミットは、text/template パッケージに対して以下の3つの主要な変更を加えています。

  1. 未初期化テンプレートに対する Lookup メソッドの動作改善: Template オブジェクトが適切に初期化されていない場合でも、Lookup メソッドが安全に動作するように修正されました。これにより、nil ポインタ参照によるパニックを防ぎます。
  2. add 処理におけるバグ修正: テンプレートのパース処理中に add 操作が行われる際、パーサーが停止した後にエラーが発生する可能性があったバグが修正されました。これは、t.add(treeSet) の呼び出し順序を変更することで対応されています。
  3. html/template のための Add メソッドの追加: html/template パッケージが text/template の機能を拡張して利用する際に、複数のテンプレートを関連付け、相互に呼び出せるようにするための Add メソッドが Template 型に追加されました。

変更の背景

Go言語の text/template パッケージは、テキストベースの出力を生成するための汎用的なテンプレートエンジンを提供します。一方、html/template パッケージは、text/template を基盤としつつ、HTML出力におけるクロスサイトスクリプティング (XSS) などのセキュリティ脆弱性を自動的にエスケープする機能を追加したものです。

このコミットが行われた2011年当時、html/template はまだ比較的新しいパッケージであり、text/template との連携においていくつかの課題を抱えていました。具体的には、以下のような問題が考えられます。

  • テンプレートのライフサイクルと状態管理: テンプレートがどのように初期化され、他のテンプレートと関連付けられるかという点で、text/template の既存の設計では html/template の要件を完全に満たせていなかった可能性があります。特に、未初期化状態のテンプレートに対する操作は、予期せぬパニックを引き起こす原因となります。
  • エラーハンドリングの厳密性: テンプレートのパース処理は複雑であり、途中でエラーが発生した場合の挙動は非常に重要です。パーサーが「停止」したと判断された後にエラーが発生すると、そのエラーが適切に伝播されず、デバッグが困難になる可能性があります。
  • html/template の特殊な要件: html/template は、セキュリティ上の理由から、複数のテンプレートを組み合わせて利用する際に、それらのテンプレートが互いに安全に参照し合えるメカニズムを必要とします。既存の text/template には、このような「相互参照」を明示的に管理する高レベルなAPIが不足していたと考えられます。

これらの背景から、html/template の堅牢性と使いやすさを向上させるために、text/template 側に基盤となる修正と機能追加が必要とされました。

前提知識の解説

このコミットを理解するためには、以下のGo言語のテンプレートパッケージに関する知識が役立ちます。

  • text/template パッケージ:
    • Go言語の標準ライブラリで提供される、テキストベースのテンプレートエンジン。
    • {{.Field}} のようなアクションを使って、データ構造のフィールドやメソッドをテンプレートに埋め込むことができます。
    • {{range .Slice}}...{{end}}{{if .Condition}}...{{end}} のような制御構造もサポートしています。
    • Template 型は、個々のテンプレートを表し、Parse メソッドでテンプレート文字列を解析します。
    • 複数のテンプレートを名前で管理し、Lookup メソッドで取得したり、Execute メソッドで実行したりできます。
    • 内部的には、テンプレート文字列を字句解析(lexing)し、構文解析(parsing)して抽象構文木(AST: Abstract Syntax Tree)を構築します。
  • html/template パッケージ:
    • text/template と同じAPIを提供しますが、HTML出力に特化しており、自動エスケープ機能(コンテキストに応じたエスケープ処理)を備えています。
    • これにより、ユーザー入力などをテンプレートに埋め込む際に、意図しないHTMLタグやJavaScriptコードが挿入されることによるXSS攻撃を防ぎます。
    • text/templateTemplate 型を内部的に利用し、その上にセキュリティ層を追加しています。
  • Template 型の common フィールド:
    • text/template パッケージの Template 型には、common という内部フィールドが存在します。これは、複数の関連するテンプレート間で共有される状態(例えば、名前付きテンプレートのマップ tmpl や関数マップ funcMap など)を保持するためのものです。
    • テンプレートが初期化されると、この common フィールドが設定され、テンプレートが「使える状態」になります。
  • parse.Treelex / parse / add / stopParse:
    • text/template の内部では、parse サブパッケージがテンプレートの構文解析を担当します。
    • lex はテンプレート文字列をトークンに分割する字句解析器です。
    • parse はトークンストリームからASTを構築します。
    • Tree 型は、解析されたテンプレートのASTを表します。
    • t.add(treeSet) は、解析されたテンプレート(t)を、関連するテンプレートのセット(treeSet)に追加する操作です。これにより、他のテンプレートから名前で参照できるようになります。
    • t.stopParse() は、パース処理の終了をマークする内部的な操作です。

技術的詳細

このコミットは、text/template パッケージの2つのファイル、src/pkg/text/template/parse/parse.gosrc/pkg/text/template/template.go に変更を加えています。

src/pkg/text/template/parse/parse.go の変更

このファイルでは、Tree 型の Parse メソッド内の処理順序が変更されています。

変更前:

func (t *Tree) Parse(...) (_ *Tree, err error) {
	defer t.recover(&err)
	t.startParse(...)
	t.parse(treeSet)
	t.stopParse() // ここでパーサーが停止
	t.add(treeSet) // その後でテンプレートを追加
	return t, nil
}

変更後:

func (t *Tree) Parse(...) (_ *Tree, err error) {
	defer t.recover(&err)
	t.startParse(...)
	t.parse(treeSet)
	t.add(treeSet) // テンプレートを追加
	t.stopParse() // その後でパーサーが停止
	return t, nil
}

この変更の目的は、コミットメッセージにある「fix bug in add: can't error after parser is stopped」を解決することです。t.add(treeSet) は、解析されたテンプレートをテンプレートセットに追加する操作であり、この操作中にエラー(例えば、同じ名前のテンプレートが既に存在する場合など)が発生する可能性があります。もし t.stopParse() が先に呼び出されてしまうと、パーサーが既に「停止」状態にあるため、add で発生したエラーが適切に処理されない、あるいは無視されてしまう可能性がありました。t.add(treeSet)t.stopParse() の前に移動することで、add 処理中に発生したエラーが defer t.recover(&err) によって捕捉され、適切に err 変数に設定されるようになります。これにより、テンプレートのパースと追加のプロセスがより堅牢になります。

src/pkg/text/template/template.go の変更

このファイルでは、Template 型に新しい Add メソッドが追加され、既存の Lookup メソッドが修正されています。

  1. Add メソッドの追加:

    // 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 {
    		return fmt.Errorf("template: redefinition of template %q", arg.name)
    	}
    	arg.common = t.common
    	t.tmpl[arg.name] = arg
    	return nil
    }
    
    • このメソッドは、引数として渡された arg テンプレートを、レシーバーである t テンプレートに関連付けます。これにより、t から arg を名前で参照できるようになります。
    • t.tmpl[arg.name] != nil のチェックにより、同じ名前のテンプレートが既に存在しないことを確認し、再定義エラーを防ぎます。
    • arg.common = t.common は非常に重要です。これにより、arg テンプレートが t テンプレートと同じ共有状態(common フィールド)を持つようになります。これは、html/template が複数のテンプレートを安全に連携させるために必要とするメカニズムの一部です。例えば、html/template では、あるテンプレートが別のテンプレートを {{template "name"}} のように呼び出す際に、両者が同じセキュリティコンテキストを共有している必要があります。common フィールドを共有することで、この要件が満たされます。
    • コメントに「Add is here to support html/template and is not intended for other uses.」と明記されており、このメソッドが html/template の特定のニーズのために導入されたことがわかります。
    • TODO: make this take a parse.Tree argument instead of a template. というコメントは、将来的には Template オブジェクトそのものではなく、より低レベルな parse.Tree を引数として受け取るように変更される可能性があることを示唆しています。これは、Add メソッドがテンプレートのASTレベルでの操作をより直接的に反映するように設計されるべきだという考えに基づいているかもしれません。
  2. Lookup メソッドの修正:

    func (t *Template) Lookup(name string) *Template {
    	if t.common == nil { // 新しく追加されたチェック
    		return nil
    	}
    	return t.tmpl[name]
    }
    
    • Lookup メソッドは、指定された名前のテンプレートを検索して返します。
    • 追加された if t.common == nil { return nil } のチェックは、コミットメッセージにある「allow Lookup to work on uninitialized templates」に対応します。
    • Template オブジェクトがまだ初期化されておらず、common フィールドが nil の場合、t.tmpl にアクセスしようとするとパニックが発生します。このチェックにより、未初期化のテンプレートに対して Lookup が呼び出された場合でも、安全に nil を返すようになります。これにより、呼び出し元は nil チェックを行うことで、テンプレートが利用可能かどうかを判断できるようになります。

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

src/pkg/text/template/parse/parse.go

--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -170,8 +170,8 @@ func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree,
 	defer t.recover(&err)
 	t.startParse(funcs, lex(t.Name, s, leftDelim, rightDelim))
 	t.parse(treeSet)
-	t.stopParse()
 	t.add(treeSet)
+	t.stopParse()
 	return t, nil
 }

src/pkg/text/template/template.go

--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -103,6 +103,23 @@ 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 {
+		return fmt.Errorf("template: redefinition of template %q", arg.name)
+	}
+	arg.common = t.common
+	t.tmpl[arg.name] = arg
+	return nil
+}
+
 // Templates returns a slice of the templates associated with t, including t
 // itself.
 func (t *Template) Templates() []*Template {
@@ -139,6 +156,9 @@ func (t *Template) Funcs(funcMap FuncMap) *Template {\n // Lookup returns the template with the given name that is associated with t,\n // or nil if there is no such template.\n func (t *Template) Lookup(name string) *Template {\n+\tif t.common == nil {\n+\t\treturn nil\n+\t}\n 	return t.tmpl[name]\n }\
 \n```

## コアとなるコードの解説

### `src/pkg/text/template/parse/parse.go` の変更点

`Parse` メソッドは、テンプレート文字列を解析し、`Tree` オブジェクトを構築する主要な関数です。この変更は、`t.add(treeSet)` と `t.stopParse()` の呼び出し順序を入れ替えることで、エラーハンドリングのタイミングを修正しています。

*   **`t.add(treeSet)`**: この関数は、現在解析中のテンプレート(`t`)を、そのテンプレートが属するテンプレートセット(`treeSet`)に追加します。これにより、他のテンプレートからこのテンプレートを名前で参照できるようになります。この操作中に、例えば同じ名前のテンプレートが既に存在する場合など、エラーが発生する可能性があります。
*   **`t.stopParse()`**: この関数は、パース処理が完了したことを内部的にマークします。通常、パース処理が停止した後は、新たなエラーを生成したり、既存のエラーを伝播させたりするメカニズムが閉じられることがあります。

変更前は、`t.stopParse()` が先に呼び出されていたため、`t.add(treeSet)` でエラーが発生しても、そのエラーが `Parse` メソッドの戻り値として適切に返されない可能性がありました。変更後は、`t.add(treeSet)` が `t.stopParse()` の前に実行されるため、`add` 処理中に発生したエラーは `defer t.recover(&err)` によって捕捉され、`Parse` メソッドの呼び出し元に正確に伝達されるようになります。これは、テンプレートのパースと登録のプロセスにおける堅牢性を高めるための重要な修正です。

### `src/pkg/text/template/template.go` の変更点

#### `Add` メソッド

`Add` メソッドは、`Template` 型に新しく追加された公開メソッドです。

```go
func (t *Template) Add(arg *Template) error {
	if t.tmpl[arg.name] != nil {
		return fmt.Errorf("template: redefinition of template %q", arg.name)
	}
	arg.common = t.common
	t.tmpl[arg.name] = arg
	return nil
}
  • if t.tmpl[arg.name] != nil: この行は、arg テンプレートの名前が、レシーバー t が管理するテンプレートマップ t.tmpl 内で既に使われていないかを確認します。もし既に存在すれば、fmt.Errorf を使って「テンプレートの再定義」エラーを返します。これは、テンプレートの名前空間の整合性を保つために重要です。
  • arg.common = t.common: この行が Add メソッドの最も重要な部分です。arg テンプレートの common フィールドを、レシーバー tcommon フィールドと同じものに設定します。前述の通り、common フィールドは複数のテンプレート間で共有される状態(テンプレートマップ、関数マップなど)を保持します。この操作により、arg テンプレートは t テンプレートと同じコンテキストを共有するようになり、特に html/template において、セキュリティコンテキストの継承や、関連するテンプレート間の安全な相互呼び出しが可能になります。
  • t.tmpl[arg.name] = arg: 最後に、arg テンプレートを t のテンプレートマップにその名前で登録します。これにより、t を通じて argLookup できるようになります。

この Add メソッドは、html/template が複数のテンプレートを組み合わせて、それらが互いに安全に参照し合えるようにするための基盤を提供します。

Lookup メソッドの修正

Lookup メソッドは、指定された名前のテンプレートを検索して返します。

func (t *Template) Lookup(name string) *Template {
	if t.common == nil {
		return nil
	}
	return t.tmpl[name]
}
  • if t.common == nil: この条件が新しく追加されました。t.commonnil であるということは、t テンプレートがまだ適切に初期化されていない状態であることを意味します。
  • return nil: 未初期化のテンプレートに対して Lookup が呼び出された場合、以前は t.tmpl[name] にアクセスしようとしてパニックを引き起こす可能性がありました。この修正により、commonnil であれば即座に nil を返すことで、安全に未初期化状態を処理し、パニックを防ぎます。呼び出し元は Lookup の戻り値が nil であるかどうかをチェックすることで、テンプレートが利用可能かどうかを判断できます。

これらの変更は、Goのテンプレートシステム、特に html/template の堅牢性と使いやすさを向上させる上で不可欠なものでした。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に text/template および html/template パッケージ)
  • コミットメッセージと差分情報
  • Go言語のテンプレートに関する一般的な解説記事
  • Go言語の common フィールドに関する議論 (Goの内部実装に関する情報源)
  • GoのテンプレートにおけるXSS対策に関する情報