[インデックス 10584] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージにおける変更です。具体的には、テンプレートが「空」であるかどうかの判定ロジック (isEmpty
関数) を、text/template
パッケージから text/template/parse
パッケージに移動し、IsEmptyTree
という名前に変更しています。これにより、テンプレートのパース処理中にテンプレートツリーのセットを構築する際に、この空判定ロジックをより適切に利用できるようになります。
変更されたファイルは以下の通りです。
src/pkg/text/template/multi_test.go
:isEmpty
関数のテストコードが削除されました。src/pkg/text/template/parse/parse.go
:IsEmptyTree
関数が新しく追加され、Tree
構造体のadd
メソッドがこの新しい関数を使用するように変更されました。src/pkg/text/template/parse/parse_test.go
:isEmpty
関数のテストコードがIsEmptyTree
のテストとして移動・追加されました。src/pkg/text/template/template.go
:isEmpty
関数が削除され、その呼び出し箇所がparse.IsEmptyTree
に変更されました。
コミット
- コミットハッシュ:
e6b3371781d4f7b07c2c7c4e2f2ef4c4e7233225
- Author: Rob Pike r@golang.org
- Date: Thu Dec 1 17:24:54 2011 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e6b3371781d4f7b07c2c7c4e2f2ef4c4e7233225
元コミット内容
template: move the empty check into parse, which needs it when constructing
tree sets.
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5449062
変更の背景
この変更の主な背景は、Goの text/template
パッケージにおけるテンプレートの再定義(redefinition)の扱いを改善することにあります。
text/template
パッケージでは、複数のテンプレートを定義し、それらを名前で管理することができます。例えば、{{define "myTemplate"}}...{{end}}
のようにテンプレートを定義します。ここで問題となるのが、同じ名前のテンプレートが複数回定義された場合の挙動です。
以前の実装では、template.go
内の isEmpty
関数が、テンプレートが実質的に空である(スペースや定義のみで、実際のコンテンツがない)かどうかを判定していました。この判定は、テンプレートの再定義を許可するかどうかを決定する際に使用されていました。具体的には、既存のテンプレートが空であり、かつ新しく定義されるテンプレートも空である場合にのみ、再定義が許可されるというロジックでした。しかし、この isEmpty
関数は template
パッケージ内に存在しており、テンプレートのパース処理を行う parse
パッケージからは直接利用できませんでした。
テンプレートのパース処理中、特に Tree
構造体の add
メソッドがテンプレートのツリーセット(map[string]*Tree
)を構築する際に、既存のテンプレートが空であるかどうかを判断する必要がありました。この判断は、テンプレートの重複定義を適切に処理するために不可欠です。parse
パッケージが isEmpty
ロジックにアクセスできないため、template
パッケージと parse
パッケージの間で不整合や冗長な処理が発生する可能性がありました。
このコミットは、isEmpty
ロジックを parse
パッケージに移動し、IsEmptyTree
として公開することで、この問題を解決します。これにより、パース時にテンプレートの空判定を直接行えるようになり、テンプレートのツリーセット構築ロジックがより堅牢で一貫性のあるものになります。
前提知識の解説
Go言語の text/template
パッケージ
text/template
パッケージは、Go言語でテキストベースのテンプレートを生成するための機能を提供します。HTML、XML、プレーンテキストなど、様々な形式のテキスト出力に利用できます。主な機能は以下の通りです。
- テンプレートの定義:
{{define "name"}}...{{end}}
構文を使って、名前付きのテンプレートを定義できます。 - アクション:
{{.Field}}
(データフィールドの表示)、{{if .Condition}}...{{end}}
(条件分岐)、{{range .Slice}}...{{end}}
(ループ)、{{template "name"}}
(他のテンプレートの呼び出し) など、様々なアクションをサポートします。 - パイプライン: 複数の関数呼び出しを
|
で連結して、データの変換を行うことができます。 - パーシング: テンプレート文字列を解析し、内部的なツリー構造(
parse.Tree
)に変換します。 - 実行: パースされたテンプレートにデータを適用し、最終的なテキスト出力を生成します。
テンプレートのツリー構造 (parse.Tree
と parse.Node
)
text/template
パッケージは、テンプレート文字列を解析する際に、その構造を抽象構文木(AST: Abstract Syntax Tree)として表現します。このASTは、parse.Tree
と parse.Node
の組み合わせで構成されます。
parse.Tree
: テンプレート全体のルートを表す構造体です。テンプレートの名前や、そのテンプレートのルートノード (Root
フィールド) を保持します。parse.Node
: ASTの各要素を表すインターフェースです。具体的なノードの種類(テキスト、アクション、条件分岐、ループなど)に応じて、ActionNode
,IfNode
,ListNode
,RangeNode
,TemplateNode
,TextNode
,WithNode
などの具象型が存在します。これらのノードは、テンプレートの構造とロジックを表現します。
テンプレートの「空」の概念
text/template
における「空のテンプレート」とは、テンプレートが実質的なコンテンツを持たず、空白文字やテンプレート定義(define
アクション)のみで構成されている状態を指します。例えば、以下のテンプレートは「空」と見なされます。
- `` (空文字列)
{{define "x"}}something{{end}}
(定義のみで、ルートテンプレートに表示されるコンテンツがない){{define "x"}}something{{end}}\n\n{{define "y"}}something{{end}}\n\n
(複数の定義と空白のみ)
一方で、hello
や {{define "x"}}something{{end}}{{if 3}}foo{{end}}
のように、実際にレンダリングされるコンテンツやアクションを含むテンプレートは「空ではない」と見なされます。
この「空」の概念は、テンプレートの再定義を許可するかどうかを決定する際に重要になります。一般的に、既にコンテンツを持つテンプレートを同じ名前で再定義することはエラーとされますが、空のテンプレートであれば、新しい定義で上書きすることが許可される場合があります。これは、テンプレートのインポートや結合のシナリオで柔軟性を提供するために利用されます。
map[string]*Tree
(ツリーセット)
map[string]*Tree
は、テンプレートの名前(文字列)をキーとし、対応する parse.Tree
構造体へのポインタを値とするマップです。これは、複数の名前付きテンプレートを管理するためのコレクションとして機能します。text/template
パッケージは、このマップを使って、定義されたすべてのテンプレートを追跡し、{{template "name"}}
アクションで他のテンプレートを呼び出す際に参照します。
技術的詳細
このコミットの技術的な核心は、テンプレートの「空」判定ロジックの責任範囲を再定義し、その実装を最適化することにあります。
変更前の isEmpty
関数
変更前は、src/pkg/text/template/template.go
に isEmpty
という関数が存在していました。この関数は parse.Node
を引数に取り、そのノードが表すテンプレートツリーが実質的に空であるかどうかを再帰的に判定していました。
// src/pkg/text/template/template.go (変更前)
func isEmpty(n parse.Node) bool {
switch n := n.(type) {
case *parse.ActionNode:
case *parse.IfNode:
case *parse.ListNode:
for _, node := range n.Nodes {
if !isEmpty(node) {
return false
}
}
return true
case *parse.RangeNode:
case *parse.TemplateNode:
case *parse.TextNode:
return len(bytes.TrimSpace(n.Text)) == 0
case *parse.WithNode:
default:
panic("unknown node: " + n.String())
}
return false
}
この実装は、ListNode
の場合はその子ノードを再帰的にチェックし、TextNode
の場合はそのテキストコンテンツが空白のみであるかを bytes.TrimSpace
を使って判定していました。その他のアクションノード(ActionNode
, IfNode
, RangeNode
, TemplateNode
, WithNode
)については、それらが存在すれば空ではないと見なされるべきですが、この switch
文では return false
が明示的に書かれていないため、デフォルトで return false
となっていました。これは、これらのノードが存在するだけでテンプレートが空ではないことを意味します。
変更後の IsEmptyTree
関数とロジックの移動
このコミットでは、上記の isEmpty
関数が src/pkg/text/template/parse/parse.go
に移動され、IsEmptyTree
という名前に変更されました。
// src/pkg/text/template/parse/parse.go (変更後)
// IsEmptyTree reports whether this tree (node) is empty of everything but space.
func IsEmptyTree(n Node) bool {
switch n := n.(type) {
case *ActionNode:
case *IfNode:
case *ListNode:
for _, node := range n.Nodes {
if !IsEmptyTree(node) {
return false
}
}
return true
case *RangeNode:
case *TemplateNode:
case *TextNode:
return len(bytes.TrimSpace(n.Text)) == 0
case *WithNode:
default:
panic("unknown node: " + n.String())
}
return false
}
関数のロジック自体はほとんど変更されていませんが、bytes
パッケージのインポートが parse.go
に追加されています。
Tree.add
メソッドの変更
src/pkg/text/template/parse/parse.go
内の Tree
構造体の add
メソッドは、パースされた新しいテンプレートツリーを treeSet
に追加する役割を担っています。このメソッドは、同じ名前のテンプレートが既に存在する場合の挙動を制御します。
変更前は、単に treeSet
に同じ名前のテンプレートが存在するかどうかを確認し、存在すればエラーを返していました。
// src/pkg/text/template/parse/parse.go (変更前)
// add adds tree to the treeSet.
func (t *Tree) add(treeSet map[string]*Tree) {
if _, present := treeSet[t.Name]; present {
t.errorf("template: multiple definition of template %q", t.Name)
}
treeSet[t.Name] = t
}
変更後、add
メソッドは IsEmptyTree
関数を利用して、既存のテンプレートまたは新しいテンプレートが空であるかどうかを考慮するようになりました。
// src/pkg/text/template/parse/parse.go (変更後)
// add adds tree to the treeSet.
func (t *Tree) add(treeSet map[string]*Tree) {
tree := treeSet[t.Name]
if tree == nil || IsEmptyTree(tree.Root) { // 既存のテンプレートが存在しないか、または空の場合
treeSet[t.Name] = t // 新しいテンプレートで上書き
return
}
if !IsEmptyTree(t.Root) { // 新しいテンプレートが空ではない場合
t.errorf("template: multiple definition of template %q", t.Name) // エラーを返す
}
// ここに到達するのは、既存のテンプレートが空ではなく、新しいテンプレートが空の場合。
// この場合、既存のテンプレートを保持し、新しい空のテンプレートは無視される。
}
この新しいロジックは以下のようになります。
- まず、
treeSet
から現在のテンプレート名に対応する既存のtree
を取得します。 - もし
tree
がnil
(つまり、まだその名前のテンプレートが定義されていない) か、またはIsEmptyTree(tree.Root)
がtrue
(既存のテンプレートが空である) の場合、新しいテンプレートt
でtreeSet
を更新します。これは、新しいテンプレートが既存の空のテンプレートを上書きすることを意味します。 - そうでない場合 (既存のテンプレートが空ではない場合)、次に新しいテンプレート
t
が空ではないかどうかを!IsEmptyTree(t.Root)
でチェックします。 - もし新しいテンプレートが空ではない場合、それは既存の非空テンプレートの再定義となるため、
t.errorf
を呼び出してエラーを報告します。 - 上記のどの条件にも当てはまらない場合 (つまり、既存のテンプレートが空ではなく、新しいテンプレートが空である場合)、
treeSet
は更新されません。これは、既存の非空テンプレートが優先され、新しい空の定義は無視されることを意味します。
この変更により、テンプレートの再定義に関するセマンティクスがより洗練され、特に define
アクションによって暗黙的に生成される空のテンプレートの扱いが改善されました。
テストコードの移動
isEmpty
関数のテストコード (isEmptyTest
構造体と TestIsEmpty
関数) も、src/pkg/text/template/multi_test.go
から src/pkg/text/template/parse/parse_test.go
に移動されました。これにより、テストもロジックの移動に合わせて適切に配置され、parse
パッケージの機能としてテストされるようになりました。
コアとなるコードの変更箇所
src/pkg/text/template/multi_test.go
isEmptyTest
構造体と TestIsEmpty
関数が完全に削除されました。
--- a/src/pkg/text/template/multi_test.go
+++ b/src/pkg/text/template/multi_test.go
@@ -13,35 +13,6 @@ import (
"text/template/parse"
)
-type isEmptyTest struct {
- name string
- input string
- empty bool
-}
-
-var isEmptyTests = []isEmptyTest{
- {"empty", ``, true},
- {"nonempty", `hello`, false},
- {"spaces only", " \t\n \t\n", true},
- {"definition", `{{define "x"}}something{{end}}`, true},
- {"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
- {"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n}}", false},
- {"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
-}
-
-func TestIsEmpty(t *testing.T) {
- for _, test := range isEmptyTests {
- template, err := New("root").Parse(test.input)
- if err != nil {
- t.Errorf("%q: unexpected error: %v", test.name, err)
- continue
- }
- if empty := isEmpty(template.Root); empty != test.empty {
- t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
- }
- }
-}
-
const (
noError = true
hasError = false
src/pkg/text/template/parse/parse.go
bytes
パッケージがインポートされ、IsEmptyTree
関数が追加されました。また、Tree
構造体の add
メソッドが変更されました。
--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -7,6 +7,7 @@
package parse
import (
+ "bytes"
"fmt"
"runtime"
"strconv"
@@ -177,10 +178,37 @@ func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree,
// add adds tree to the treeSet.
func (t *Tree) add(treeSet map[string]*Tree) {
- if _, present := treeSet[t.Name]; present {
+ tree := treeSet[t.Name]
+ if tree == nil || IsEmptyTree(tree.Root) {
+ treeSet[t.Name] = t
+ return
+ }
+ if !IsEmptyTree(t.Root) {
t.errorf("template: multiple definition of template %q", t.Name)
}
- treeSet[t.Name] = t
+}
+
+// IsEmptyTree reports whether this tree (node) is empty of everything but space.
+func IsEmptyTree(n Node) bool {
+ switch n := n.(type) {
+ case *ActionNode:
+ case *IfNode:
+ case *ListNode:
+ for _, node := range n.Nodes {
+ if !IsEmptyTree(node) {
+ return false
+ }
+ }
+ return true
+ case *RangeNode:
+ case *TemplateNode:
+ case *TextNode:
+ return len(bytes.TrimSpace(n.Text)) == 0
+ case *WithNode:
+ default:
+ panic("unknown node: " + n.String())
+ }
+ return false
}
// parse is the top-level parser for a template, essentially the same
src/pkg/text/template/parse/parse_test.go
isEmptyTest
構造体と TestIsEmpty
関数が追加されました。
--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -257,3 +257,32 @@ func TestParse(t *testing.T) {
}
}
}
+
+type isEmptyTest struct {
+ name string
+ input string
+ empty bool
+}
+
+var isEmptyTests = []isEmptyTest{
+ {"empty", ``, true},
+ {"nonempty", `hello`, false},
+ {"spaces only", " \t\n \t\n", true},
+ {"definition", `{{define "x"}}something{{end}}`, true},
+ {"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
+ {"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n}}", false},
+ {"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
+}
+
+func TestIsEmpty(t *testing.T) {
+ for _, test := range isEmptyTests {
+ tree, err := New("root").Parse(test.input, "", "", make(map[string]*Tree), nil)
+ if err != nil {
+ t.Errorf("%q: unexpected error: %v", test.name, err)
+ continue
+ }
+ if empty := IsEmptyTree(tree.Root); empty != test.empty {
+ t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
+ }
+ }
+}
src/pkg/text/template/template.go
bytes
パッケージのインポートが削除され、isEmpty
関数が削除されました。associate
メソッド内の isEmpty
呼び出しが parse.IsEmptyTree
に変更されました。
--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -5,7 +5,6 @@
package template
import (
- "bytes"
"fmt"
"reflect"
"text/template/parse"
@@ -198,8 +197,8 @@ func (t *Template) associate(new *Template) error {\n }\n name := new.name\n if old := t.tmpl[name]; old != nil {\n- oldIsEmpty := isEmpty(old.Root)\n- newIsEmpty := isEmpty(new.Root)\n+ oldIsEmpty := parse.IsEmptyTree(old.Root)\n+ newIsEmpty := parse.IsEmptyTree(new.Root)\n if !oldIsEmpty && !newIsEmpty {\n return fmt.Errorf("template: redefinition of template %q", name)\n }\
@@ -211,26 +210,3 @@ func (t *Template) associate(new *Template) error {\
t.tmpl[name] = new
return nil
}
-
-// isEmpty reports whether this tree (node) is empty of everything but space.
-func isEmpty(n parse.Node) bool {
- switch n := n.(type) {
- case *parse.ActionNode:
- case *parse.IfNode:
- case *parse.ListNode:
- for _, node := range n.Nodes {
- if !isEmpty(node) {
- return false
- }
- }
- return true
- case *parse.RangeNode:
- case *parse.TemplateNode:
- case *parse.TextNode:
- return len(bytes.TrimSpace(n.Text)) == 0
- case *parse.WithNode:
- default:
- panic("unknown node: " + n.String())
- }
- return false
-}
コアとなるコードの解説
src/pkg/text/template/parse/parse.go
の変更
IsEmptyTree
関数の追加: この関数は、テンプレートのASTノードが実質的に空であるかどうかを判定します。text/template
パッケージのisEmpty
関数とほぼ同じロジックですが、parse
パッケージ内で定義されることで、テンプレートのパース処理中に直接利用できるようになりました。特に、TextNode
の内容が空白のみであるかを判定するためにbytes.TrimSpace
を使用しています。Tree.add
メソッドの変更: この変更は、テンプレートの再定義ロジックの核心です。tree := treeSet[t.Name]
で、追加しようとしているテンプレートと同じ名前の既存のテンプレートを取得します。if tree == nil || IsEmptyTree(tree.Root)
: もし既存のテンプレートが存在しないか、または既存のテンプレートが空である場合、新しいテンプレートt
でtreeSet
を更新します。これは、新しいテンプレートが既存の空のテンプレートを上書きすることを意味します。if !IsEmptyTree(t.Root)
: 上記の条件に当てはまらず、かつ新しいテンプレートt
が空ではない場合、それは既存の非空テンプレートの再定義となるため、エラーを発生させます。 このロジックにより、空のテンプレートは新しい定義で上書き可能ですが、非空のテンプレートは再定義できないという、より柔軟かつ堅牢なテンプレート管理が可能になります。
src/pkg/text/template/multi_test.go
と src/pkg/text/template/parse/parse_test.go
の変更
- テストコードの移動:
isEmpty
関数のテストがmulti_test.go
から削除され、parse_test.go
にIsEmptyTree
のテストとして移動されました。これは、IsEmptyTree
がparse
パッケージの機能となったため、そのテストも同じパッケージ内に配置されるべきであるという原則に従っています。これにより、テストの責務が明確になり、コードの構造がより論理的になります。
src/pkg/text/template/template.go
の変更
isEmpty
関数の削除:isEmpty
関数がparse
パッケージに移動されたため、template.go
からは削除されました。associate
メソッドの変更:associate
メソッド内でisEmpty
を呼び出していた箇所が、parse.IsEmptyTree
に変更されました。これにより、template
パッケージはparse
パッケージが提供する空判定ロジックを利用するようになり、依存関係が整理されました。
これらの変更により、isEmpty
のロジックがテンプレートのパースとツリー構築の責任を持つ parse
パッケージに集約され、text/template
パッケージ全体のコードの凝集度と保守性が向上しました。
関連リンク
- Go言語の
text/template
パッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語の
text/template/parse
パッケージ公式ドキュメント: https://pkg.go.dev/text/template/parse
(注: 元コミット内容に記載されていた https://golang.org/cl/5449062
のリンクは、現在のGoのChange Listシステムでは見つかりませんでした。これは、GoのCLシステムが時間とともに変更されたか、またはリンクが古くなっているためと考えられます。)
参考にした情報源リンク
- Go言語の公式ドキュメント (
pkg.go.dev
) - Go言語のソースコード (GitHubリポジトリ)
- Go言語のテンプレートに関する一般的な情報源
bytes.TrimSpace
関数のドキュメント- 抽象構文木 (AST) に関する一般的な知識
- Go言語におけるパッケージ設計の原則