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

[インデックス 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.Treeparse.Node)

text/template パッケージは、テンプレート文字列を解析する際に、その構造を抽象構文木(AST: Abstract Syntax Tree)として表現します。このASTは、parse.Treeparse.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.goisEmpty という関数が存在していました。この関数は 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) // エラーを返す
	}
	// ここに到達するのは、既存のテンプレートが空ではなく、新しいテンプレートが空の場合。
	// この場合、既存のテンプレートを保持し、新しい空のテンプレートは無視される。
}

この新しいロジックは以下のようになります。

  1. まず、treeSet から現在のテンプレート名に対応する既存の tree を取得します。
  2. もし treenil (つまり、まだその名前のテンプレートが定義されていない) か、または IsEmptyTree(tree.Root)true (既存のテンプレートが空である) の場合、新しいテンプレート ttreeSet を更新します。これは、新しいテンプレートが既存の空のテンプレートを上書きすることを意味します。
  3. そうでない場合 (既存のテンプレートが空ではない場合)、次に新しいテンプレート t が空ではないかどうかを !IsEmptyTree(t.Root) でチェックします。
  4. もし新しいテンプレートが空ではない場合、それは既存の非空テンプレートの再定義となるため、t.errorf を呼び出してエラーを報告します。
  5. 上記のどの条件にも当てはまらない場合 (つまり、既存のテンプレートが空ではなく、新しいテンプレートが空である場合)、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): もし既存のテンプレートが存在しないか、または既存のテンプレートが空である場合、新しいテンプレート ttreeSet を更新します。これは、新しいテンプレートが既存の空のテンプレートを上書きすることを意味します。 if !IsEmptyTree(t.Root): 上記の条件に当てはまらず、かつ新しいテンプレート t が空ではない場合、それは既存の非空テンプレートの再定義となるため、エラーを発生させます。 このロジックにより、空のテンプレートは新しい定義で上書き可能ですが、非空のテンプレートは再定義できないという、より柔軟かつ堅牢なテンプレート管理が可能になります。

src/pkg/text/template/multi_test.gosrc/pkg/text/template/parse/parse_test.go の変更

  • テストコードの移動: isEmpty 関数のテストが multi_test.go から削除され、parse_test.goIsEmptyTree のテストとして移動されました。これは、IsEmptyTreeparse パッケージの機能となったため、そのテストも同じパッケージ内に配置されるべきであるという原則に従っています。これにより、テストの責務が明確になり、コードの構造がより論理的になります。

src/pkg/text/template/template.go の変更

  • isEmpty 関数の削除: isEmpty 関数が parse パッケージに移動されたため、template.go からは削除されました。
  • associate メソッドの変更: associate メソッド内で isEmpty を呼び出していた箇所が、parse.IsEmptyTree に変更されました。これにより、template パッケージは parse パッケージが提供する空判定ロジックを利用するようになり、依存関係が整理されました。

これらの変更により、isEmpty のロジックがテンプレートのパースとツリー構築の責任を持つ parse パッケージに集約され、text/template パッケージ全体のコードの凝集度と保守性が向上しました。

関連リンク

(注: 元コミット内容に記載されていた https://golang.org/cl/5449062 のリンクは、現在のGoのChange Listシステムでは見つかりませんでした。これは、GoのCLシステムが時間とともに変更されたか、またはリンクが古くなっているためと考えられます。)

参考にした情報源リンク

  • Go言語の公式ドキュメント (pkg.go.dev)
  • Go言語のソースコード (GitHubリポジトリ)
  • Go言語のテンプレートに関する一般的な情報源
  • bytes.TrimSpace 関数のドキュメント
  • 抽象構文木 (AST) に関する一般的な知識
  • Go言語におけるパッケージ設計の原則