[インデックス 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つの主要な変更を加えています。
- 未初期化テンプレートに対する
Lookup
メソッドの動作改善:Template
オブジェクトが適切に初期化されていない場合でも、Lookup
メソッドが安全に動作するように修正されました。これにより、nil
ポインタ参照によるパニックを防ぎます。 add
処理におけるバグ修正: テンプレートのパース処理中にadd
操作が行われる際、パーサーが停止した後にエラーが発生する可能性があったバグが修正されました。これは、t.add(treeSet)
の呼び出し順序を変更することで対応されています。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/template
のTemplate
型を内部的に利用し、その上にセキュリティ層を追加しています。
Template
型のcommon
フィールド:text/template
パッケージのTemplate
型には、common
という内部フィールドが存在します。これは、複数の関連するテンプレート間で共有される状態(例えば、名前付きテンプレートのマップtmpl
や関数マップfuncMap
など)を保持するためのものです。- テンプレートが初期化されると、この
common
フィールドが設定され、テンプレートが「使える状態」になります。
parse.Tree
とlex
/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.go
と src/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
メソッドが修正されています。
-
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レベルでの操作をより直接的に反映するように設計されるべきだという考えに基づいているかもしれません。
- このメソッドは、引数として渡された
-
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
フィールドを、レシーバーt
のcommon
フィールドと同じものに設定します。前述の通り、common
フィールドは複数のテンプレート間で共有される状態(テンプレートマップ、関数マップなど)を保持します。この操作により、arg
テンプレートはt
テンプレートと同じコンテキストを共有するようになり、特にhtml/template
において、セキュリティコンテキストの継承や、関連するテンプレート間の安全な相互呼び出しが可能になります。t.tmpl[arg.name] = arg
: 最後に、arg
テンプレートをt
のテンプレートマップにその名前で登録します。これにより、t
を通じてarg
をLookup
できるようになります。
この 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.common
がnil
であるということは、t
テンプレートがまだ適切に初期化されていない状態であることを意味します。return nil
: 未初期化のテンプレートに対してLookup
が呼び出された場合、以前はt.tmpl[name]
にアクセスしようとしてパニックを引き起こす可能性がありました。この修正により、common
がnil
であれば即座にnil
を返すことで、安全に未初期化状態を処理し、パニックを防ぎます。呼び出し元はLookup
の戻り値がnil
であるかどうかをチェックすることで、テンプレートが利用可能かどうかを判断できます。
これらの変更は、Goのテンプレートシステム、特に html/template
の堅牢性と使いやすさを向上させる上で不可欠なものでした。
関連リンク
- Go言語の
text/template
パッケージのドキュメント: https://pkg.go.dev/text/template - Go言語の
html/template
パッケージのドキュメント: https://pkg.go.dev/html/template - Go言語のテンプレートに関する公式ブログ記事 (古いものですが、概念理解に役立ちます): https://go.dev/blog/go-and-html-templates
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
text/template
およびhtml/template
パッケージ) - コミットメッセージと差分情報
- Go言語のテンプレートに関する一般的な解説記事
- Go言語の
common
フィールドに関する議論 (Goの内部実装に関する情報源) - GoのテンプレートにおけるXSS対策に関する情報