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

[インデックス 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 パッケージに関する基本的な概念を理解しておく必要があります。

  1. text/template パッケージ: Go言語の標準ライブラリの一つで、テキストベースのテンプレートを処理するために使用されます。HTMLやXMLなどの構造化されたテキストだけでなく、任意のテキスト形式の生成に利用できます。
  2. テンプレート (Template): プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストファイルまたは文字列です。データが適用されると、プレースホルダーが実際の値に置き換えられ、制御構造が実行されて最終的な出力が生成されます。
  3. アクション (Actions): テンプレート内で {{...}} で囲まれた部分を指します。これらはGoのコードとして評価され、データの表示、関数の呼び出し、制御構造の実行などを行います。
    • {{define "name"}}...{{end}}: 名前付きテンプレートを定義するアクションです。定義されたテンプレートは、後で {{template "name"}} アクションを使って他のテンプレートから呼び出すことができます。これにより、テンプレートの再利用性が高まります。
    • {{template "name"}}: 定義済みの名前付きテンプレートを呼び出すアクションです。
  4. パース (Parsing): テンプレート文字列を読み込み、その構文を解析して、コンピュータが理解できる内部表現(通常は抽象構文木: AST)に変換するプロセスです。この内部表現が、後でデータと結合されて最終的な出力を生成するために使用されます。
  5. テンプレートセット (Template Set): 複数の名前付きテンプレートをまとめて管理するための概念です。text/template パッケージでは、Set 型がこれに該当します。これにより、関連するテンプレート群を一つの単位として扱い、名前の衝突を避けつつ効率的に管理できます。
  6. 抽象構文木 (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 の通常のテンプレート解析ロジックと重複し、コードの冗長性を生んでいました。

このコミットでは、以下の主要な変更が行われました。

  1. parse.Parse 関数のシグネチャ変更: src/pkg/text/template/parse/parse.goParse 関数に 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)
    
  2. parse メソッドの統合: src/pkg/text/template/parse/parse.go(*Tree).parse メソッドが大幅に書き換えられました。以前は toEOF フラグに基づいてEOFまでパースするかどうかを制御していましたが、新しい実装では、トップレベルのパースとして {{define}} アクションを特別に処理するようになりました。 parse メソッドは、入力ストリームを走査し、itemLeftDelim (つまり {{) の後に itemDefine が続く場合、それはテンプレート定義の開始であると認識します。そして、parseDefinition という新しいヘルパーメソッドを呼び出して、その define ブロックの内容を解析し、treeSet に追加します。

  3. parseDefinition メソッドの導入: src/pkg/text/template/parse/parse.goparseDefinition という新しいメソッドが追加されました。このメソッドは、{{define "name"}}...{{end}} ブロックの具体的な解析を担当します。

    • define キーワードの後に続くテンプレート名(文字列リテラル)を抽出し、strconv.Unquote でクォートを解除します。
    • テンプレート名が既に treeSet に存在する場合は、多重定義エラーを報告します。
    • {{define ...}} の閉じデリミタ (}}) の後から {{end}} までの内容を itemList メソッドでパースし、その結果を新しい TreeRoot ノードとして設定します。
    • 最終的に、解析された Tree を、その名前をキーとして treeSet に格納します。
  4. parse/set.go の簡素化: 最も大きな変更の一つは、src/pkg/text/template/parse/set.goSet 関数です。変更前は、この関数が define ブロックを個別にスキャンし、パースする複雑なロジックを持っていました。 変更後、このロジックは完全に削除され、代わりに parse.New("ROOT").Parse(...) を呼び出すだけになりました。これは、parse.Parse 関数自体が define ブロックの解析を処理するようになったため、Set 関数は単にトップレベルのテンプレートをパースするだけでよくなったことを意味します。Set 関数は、parse.Parse に渡す treeSet 引数として、自身が管理する map[string]*Tree を渡すことで、define されたテンプレートが自動的にそのマップに追加されるようにします。

  5. text/template/parse.go の変更: Template.Parse および Template.ParseInSet メソッドの parse.New(...).Parse(...) の呼び出しが、新しい parse.Parse のシグネチャに合わせて更新されました。特に ParseInSet では、set.trees (新しい Set 構造体のフィールド) が parse.Parse に渡されるようになりました。

  6. text/template/set.go の変更: Set 構造体に trees map[string]*parse.Tree という新しいフィールドが追加されました。このマップは、parse パッケージによって管理され、Set に属する名前付きテンプレートのASTを直接保持します。これにより、Set はテンプレートのASTを直接参照できるようになり、ParseInSet での add メソッドの呼び出しが簡素化されました。

これらの変更により、{{define}} ブロックの解析は、テンプレートのコアなパースロジックに一元化され、Set パッケージは解析の詳細から解放されました。これにより、コードベースがよりクリーンになり、将来的な機能拡張やAPIの統一が容易になります。

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

このコミットのコアとなる変更は、主に以下のファイルに集中しています。

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

    • (*Tree).Parse 関数のシグネチャ変更と、treeSet 引数の追加。
    • (*Tree).parse メソッドの大幅な書き換え。{{define}} ブロックを検出して parseDefinition を呼び出すロジックが追加されました。
    • 新しく (*Tree).parseDefinition メソッドが追加され、{{define}} ブロックの具体的な解析と treeSet への登録を担当します。
    • itemList メソッドから toEOF フラグが削除され、{{end}}{{else}} で終了するロジックが簡素化されました。
  2. src/pkg/text/template/parse/set.go:

    • Set 関数の実装が大幅に簡素化されました。以前の複雑な define ブロックの個別解析ロジックが削除され、parse.New("ROOT").Parse(...) を呼び出すだけになりました。
  3. 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.ParsetreeSet 引数として渡され、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 はその詳細から解放されました。これは、コードの重複を排除し、モジュール間の責任をより明確にするための重要なリファクタリングです。

関連リンク

参考にした情報源リンク

  • 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 パッケージの機能と構造を理解しました。