[インデックス 10445] ファイルの概要
このコミットは、Go言語の標準ライブラリ text/template パッケージにおけるテンプレートのパース(解析)処理、特に {{define}} ブロックの扱いに関する重要なリファクタリングです。これまで {{define}} ブロックはテンプレートセット(Set)に特化した方法で個別に解析されていましたが、この変更により、通常のテンプレート解析プロセスの中で {{define}} ブロックも統合的に解析されるようになりました。これにより、コードの重複が削減され、将来的なテンプレートとセットのAPI統合への道が開かれました。
コミット
commit 25d2987dd93e1fa0d325af440a69e26fc0c9ee0e
Author: Rob Pike <r@golang.org>
Date: Thu Nov 17 22:53:23 2011 -0800
text/template: refactor set parsing
Parse {{define}} blocks during template parsing rather than separately as a set-specific thing.
This cleans up set parse significantly, and enables the next step, if we want, to unify the
API for templates and sets.
Other than an argument change to parse.Parse, which is in effect an internal function and
unused by client code, there is no API change and no spec change yet.
R=golang-dev, rsc, r
CC=golang-dev
https://golang.org/cl/5393049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/25d2987dd93e1fa0d325af440a69e26fc0c9ee0e
元コミット内容
text/template パッケージにおいて、セットのパース処理をリファクタリングしました。
{{define}} ブロックを、セットに特化した個別の処理としてではなく、テンプレートのパース中に解析するように変更しました。
これにより、セットのパース処理が大幅に整理され、もし望むのであれば、テンプレートとセットのAPIを統合するための次のステップが可能になります。
parse.Parse の引数変更を除けば、クライアントコードからは使用されない内部関数であるため、APIの変更や仕様の変更はまだありません。
変更の背景
このコミットの主な背景は、text/template パッケージにおけるコードの重複と複雑性の解消、そして将来的なAPIの統一性向上です。
Goの text/template パッケージは、テキストベースのテンプレートを生成するための強力なツールです。このパッケージでは、{{define "name"}}...{{end}} のような構文を使って、名前付きのテンプレートブロックを定義し、後で {{template "name"}} で再利用することができます。
変更前は、{{define}} ブロックの解析ロジックが、通常のテンプレート解析とは別に、特にテンプレートの「セット」(Set)を扱う部分で重複して存在していました。これは、Set が複数の名前付きテンプレートを管理するための構造であり、その中に define されたテンプレートを格納する必要があったためです。
この重複は、コードの保守性を低下させ、新しい機能を追加する際の複雑性を増大させていました。コミットメッセージにあるように、「セットのパース処理を大幅に整理し、もし望むのであれば、テンプレートとセットのAPIを統合するための次のステップを可能にする」ことが、このリファクタリングの動機です。つまり、define ブロックの解析をテンプレートのコア解析ロジックに統合することで、よりクリーンで一貫性のある設計を目指しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の text/template パッケージに関する基本的な概念を理解しておく必要があります。
text/templateパッケージ: Go言語の標準ライブラリの一つで、テキストベースのテンプレートを処理するために使用されます。HTMLやXMLなどの構造化されたテキストだけでなく、任意のテキスト形式の生成に利用できます。- テンプレート (Template): プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストファイルまたは文字列です。データが適用されると、プレースホルダーが実際の値に置き換えられ、制御構造が実行されて最終的な出力が生成されます。
- アクション (Actions): テンプレート内で
{{...}}で囲まれた部分を指します。これらはGoのコードとして評価され、データの表示、関数の呼び出し、制御構造の実行などを行います。{{define "name"}}...{{end}}: 名前付きテンプレートを定義するアクションです。定義されたテンプレートは、後で{{template "name"}}アクションを使って他のテンプレートから呼び出すことができます。これにより、テンプレートの再利用性が高まります。{{template "name"}}: 定義済みの名前付きテンプレートを呼び出すアクションです。
- パース (Parsing): テンプレート文字列を読み込み、その構文を解析して、コンピュータが理解できる内部表現(通常は抽象構文木: AST)に変換するプロセスです。この内部表現が、後でデータと結合されて最終的な出力を生成するために使用されます。
- テンプレートセット (Template Set): 複数の名前付きテンプレートをまとめて管理するための概念です。
text/templateパッケージでは、Set型がこれに該当します。これにより、関連するテンプレート群を一つの単位として扱い、名前の衝突を避けつつ効率的に管理できます。 - 抽象構文木 (Abstract Syntax Tree: AST): ソースコードの抽象的な構文構造を木構造で表現したものです。パースの出力として生成され、コンパイラやインタプリタがコードの意味を理解し、処理するために利用します。
text/templateパッケージも、テンプレート文字列をパースしてASTを構築します。
このコミットは、特に {{define}} アクションがASTに変換される過程と、それがテンプレートセットにどのように関連付けられるかという内部的なパースロジックの変更に焦点を当てています。
技術的詳細
このコミットの核心は、{{define}} ブロックの解析ロジックを text/template/parse パッケージのコアなパース処理に統合した点にあります。
変更前は、text/template/parse/set.go 内の Set 関数が、テンプレート文字列全体をスキャンし、{{define}} ブロックを個別に抽出し、それぞれを新しい Tree としてパースしていました。これは、Set が複数のテンプレートを管理するという性質上、各 define ブロックが独立したテンプレートとして扱われる必要があったためです。しかし、このアプローチは、define ブロックの解析ロジックが parse/parse.go の通常のテンプレート解析ロジックと重複し、コードの冗長性を生んでいました。
このコミットでは、以下の主要な変更が行われました。
-
parse.Parse関数のシグネチャ変更:src/pkg/text/template/parse/parse.goのParse関数にtreeSet map[string]*Treeという新しい引数が追加されました。このtreeSetは、パース中に見つかった{{define}}ブロックによって定義された名前付きテンプレートのASTを格納するためのマップです。これにより、Parse関数自体がdefineブロックを認識し、その内容をtreeSetに追加できるようになります。変更前:
func (t *Tree) Parse(s, leftDelim, rightDelim string, funcs ...map[string]interface{}) (tree *Tree, err error)変更後:
func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) -
parseメソッドの統合:src/pkg/text/template/parse/parse.goの(*Tree).parseメソッドが大幅に書き換えられました。以前はtoEOFフラグに基づいてEOFまでパースするかどうかを制御していましたが、新しい実装では、トップレベルのパースとして{{define}}アクションを特別に処理するようになりました。parseメソッドは、入力ストリームを走査し、itemLeftDelim(つまり{{) の後にitemDefineが続く場合、それはテンプレート定義の開始であると認識します。そして、parseDefinitionという新しいヘルパーメソッドを呼び出して、そのdefineブロックの内容を解析し、treeSetに追加します。 -
parseDefinitionメソッドの導入:src/pkg/text/template/parse/parse.goにparseDefinitionという新しいメソッドが追加されました。このメソッドは、{{define "name"}}...{{end}}ブロックの具体的な解析を担当します。defineキーワードの後に続くテンプレート名(文字列リテラル)を抽出し、strconv.Unquoteでクォートを解除します。- テンプレート名が既に
treeSetに存在する場合は、多重定義エラーを報告します。 {{define ...}}の閉じデリミタ (}}) の後から{{end}}までの内容をitemListメソッドでパースし、その結果を新しいTreeのRootノードとして設定します。- 最終的に、解析された
Treeを、その名前をキーとしてtreeSetに格納します。
-
parse/set.goの簡素化: 最も大きな変更の一つは、src/pkg/text/template/parse/set.goのSet関数です。変更前は、この関数がdefineブロックを個別にスキャンし、パースする複雑なロジックを持っていました。 変更後、このロジックは完全に削除され、代わりにparse.New("ROOT").Parse(...)を呼び出すだけになりました。これは、parse.Parse関数自体がdefineブロックの解析を処理するようになったため、Set関数は単にトップレベルのテンプレートをパースするだけでよくなったことを意味します。Set関数は、parse.Parseに渡すtreeSet引数として、自身が管理するmap[string]*Treeを渡すことで、defineされたテンプレートが自動的にそのマップに追加されるようにします。 -
text/template/parse.goの変更:Template.ParseおよびTemplate.ParseInSetメソッドのparse.New(...).Parse(...)の呼び出しが、新しいparse.Parseのシグネチャに合わせて更新されました。特にParseInSetでは、set.trees(新しいSet構造体のフィールド) がparse.Parseに渡されるようになりました。 -
text/template/set.goの変更:Set構造体にtrees map[string]*parse.Treeという新しいフィールドが追加されました。このマップは、parseパッケージによって管理され、Setに属する名前付きテンプレートのASTを直接保持します。これにより、SetはテンプレートのASTを直接参照できるようになり、ParseInSetでのaddメソッドの呼び出しが簡素化されました。
これらの変更により、{{define}} ブロックの解析は、テンプレートのコアなパースロジックに一元化され、Set パッケージは解析の詳細から解放されました。これにより、コードベースがよりクリーンになり、将来的な機能拡張やAPIの統一が容易になります。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
-
src/pkg/text/template/parse/parse.go:(*Tree).Parse関数のシグネチャ変更と、treeSet引数の追加。(*Tree).parseメソッドの大幅な書き換え。{{define}}ブロックを検出してparseDefinitionを呼び出すロジックが追加されました。- 新しく
(*Tree).parseDefinitionメソッドが追加され、{{define}}ブロックの具体的な解析とtreeSetへの登録を担当します。 itemListメソッドからtoEOFフラグが削除され、{{end}}や{{else}}で終了するロジックが簡素化されました。
-
src/pkg/text/template/parse/set.go:Set関数の実装が大幅に簡素化されました。以前の複雑なdefineブロックの個別解析ロジックが削除され、parse.New("ROOT").Parse(...)を呼び出すだけになりました。
-
src/pkg/text/template/set.go:Set構造体にtrees map[string]*parse.Treeフィールドが追加されました。
コアとなるコードの解説
src/pkg/text/template/parse/parse.go の変更
// Parse parses the template definition string to construct an internal
// representation of the template for execution. If either action delimiter
// string is empty, the default ("{{" or "}}") is used.
func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
defer t.recover(&err)
t.startParse(funcs, lex(t.Name, s, leftDelim, rightDelim))
t.parse(treeSet) // treeSet が新しい引数として追加
t.stopParse()
return t, nil
}
// parse is the top-level parser for a template, essentially the same
// as itemList except it also parses {{define}} actions.
// It runs to EOF.
func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
t.Root = newList()
for t.peek().typ != itemEOF {
if t.peek().typ == itemLeftDelim {
delim := t.next()
if t.next().typ == itemDefine { // {{define}} を検出
newT := New("new definition") // 新しい Tree を作成
newT.startParse(t.funcs, t.lex)
newT.parseDefinition(treeSet) // parseDefinition を呼び出す
continue
}
t.backup2(delim)
}
n := t.textOrAction()
if n.Type() == nodeEnd {
t.errorf("unexpected %s", n)
}
t.Root.append(n)
}
return nil
}
// parseDefinition parses a {{define}} ... {{end}} template definition and
// installs the definition in the treeSet map. The "define" keyword has already
// been scanned.
func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
if treeSet == nil {
t.errorf("no set specified for template definition")
}
const context = "define clause"
name := t.expect(itemString, context) // テンプレート名を取得
var err error
t.Name, err = strconv.Unquote(name.val) // クォートを解除
if err != nil {
t.error(err)
}
t.expect(itemRightDelim, context) // }} を期待
var end Node
t.Root, end = t.itemList() // define ブロックの内容をパース
if end.Type() != nodeEnd {
t.errorf("unexpected %s in %s", end, context)
}
t.stopParse()
if _, present := treeSet[t.Name]; present {
t.errorf("template: %q multiply defined", name) // 多重定義チェック
}
treeSet[t.Name] = t // treeSet に登録
}
Parse 関数は、テンプレート文字列を解析し、ASTを構築するエントリポイントです。新しい treeSet 引数は、{{define}} ブロックによって定義されたサブテンプレートを格納するために使用されます。
parse メソッドは、テンプレートのトップレベルの解析ループです。ここで {{define}} アクションが検出されると、新しい Tree オブジェクトが作成され、その parseDefinition メソッドが呼び出されます。
parseDefinition メソッドは、{{define "name"}}...{{end}} 構文の内部を解析します。テンプレート名を抽出し、その名前で treeSet に新しい Tree を登録します。これにより、define ブロックが通常のテンプレート解析フローの中で処理されるようになります。
src/pkg/text/template/parse/set.go の変更
// Set returns a slice of Trees created by parsing the template set
// definition in the argument string. If an error is encountered,
// parsing stops and an empty slice is returned with the error.
func Set(text, leftDelim, rightDelim string, funcs ...map[string]interface{}) (tree map[string]*Tree, err error) {
tree = make(map[string]*Tree)
// Top-level template name is needed but unused. TODO: clean this up.
_, err = New("ROOT").Parse(text, leftDelim, rightDelim, tree, funcs...) // 簡素化された呼び出し
return
}
Set 関数は、以前は define ブロックを個別に解析する複雑なロジックを持っていましたが、このコミットにより、そのロジックは parse/parse.go に移動しました。そのため、Set 関数は単に New("ROOT").Parse(...) を呼び出すだけでよくなりました。ここで tree マップが parse.Parse の treeSet 引数として渡され、define されたテンプレートが自動的にこのマップに登録されるようになります。
src/pkg/text/template/set.go の変更
type Set struct {
tmpl map[string]*Template
trees map[string]*parse.Tree // maintained by parse package (新しいフィールド)
leftDelim string
rightDelim string
parseFuncs FuncMap
}
Set 構造体に trees map[string]*parse.Tree という新しいフィールドが追加されました。これは、parse パッケージが define されたテンプレートのASTを直接このマップに格納するために使用されます。これにより、Set は解析されたテンプレートツリーへの直接的な参照を持つことができ、Template.ParseInSet での set.add(t) の呼び出しが簡素化されます。
これらの変更により、{{define}} ブロックの解析は text/template/parse パッケージのコアなパースロジックに一元化され、text/template/parse/set.go はその詳細から解放されました。これは、コードの重複を排除し、モジュール間の責任をより明確にするための重要なリファクタリングです。
関連リンク
- Go言語
text/templateパッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語
text/template/parseパッケージ公式ドキュメント: https://pkg.go.dev/text/template/parse
参考にした情報源リンク
- GitHub: golang/go commit 25d2987dd93e1fa0d325af440a69e26fc0c9ee0e: https://github.com/golang/go/commit/25d2987dd93e1fa0d325af440a69e26fc0c9ee0e
- Gerrit Code Review:
https://golang.org/cl/5393049(コミットメッセージに記載されている変更リストのURL) - Go言語の公式ドキュメント (
pkg.go.dev) を参照し、text/templateおよびtext/template/parseパッケージの機能と構造を理解しました。