[インデックス 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
パッケージの機能と構造を理解しました。