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

[インデックス 15609] ファイルの概要

このコミットは、Go言語の text/template パッケージにおいて、空のテンプレートを実行しようとした際のエラー報告を改善することを目的としています。具体的には、エラーメッセージに、定義されている他のテンプレートの名前を含めることで、デバッグの際に役立つ情報を提供するようになります。これにより、ユーザーが誤って空のテンプレートを実行してしまった場合に、どのテンプレートが利用可能であるかをより明確に把握できるようになります。

コミット

commit c76379954f57399b2e84528ac369f5cb07698acf
Author: Rob Pike <r@golang.org>
Date:   Wed Mar 6 12:34:19 2013 -0800

    text/template: improve error reporting for executing an empty template
    Fixes #4522.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7502044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c76379954f57399b2e84528ac369f5cb07698acf

元コミット内容

text/template: improve error reporting for executing an empty template
Fixes #4522.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7502044

変更の背景

この変更は、GoのIssue #4522に対応するものです。元のIssueでは、text/template パッケージでテンプレートをパースしたものの、そのテンプレートが実際には空であったり、名前が付けられていない場合に、Execute メソッドが「"" is an incomplete or empty template」というエラーを返していました。このエラーメッセージは、どのテンプレートが問題なのか、あるいは他に利用可能なテンプレートがあるのかについての情報が不足しており、デバッグを困難にしていました。

特に、複数のテンプレートが定義されている Template オブジェクトに対して、誤って存在しないテンプレート名で ExecuteTemplate を呼び出したり、メインのテンプレートが空であったりした場合に、ユーザーはどのテンプレートが利用可能であるかを知る術がありませんでした。このコミットは、このエラーメッセージを改善し、定義されている他のテンプレートの名前をエラーメッセージに含めることで、ユーザーが問題をより迅速に特定できるようにすることを目的としています。

前提知識の解説

Go言語の text/template パッケージ

text/template パッケージは、Go言語でテキストベースのテンプレートを生成するための機能を提供します。これは、HTML、XML、プレーンテキストなどの動的なコンテンツを生成する際に非常に便利です。

  • Template 構造体: テンプレートの定義と実行を管理する主要な構造体です。
  • Parse メソッド: テンプレート文字列を解析し、内部的なツリー構造(TreeおよびRootフィールド)を構築します。
  • Execute メソッド: パースされたテンプレートを指定されたデータと io.Writer に適用し、結果を書き出します。
  • ExecuteTemplate メソッド: 名前付きテンプレートを実行します。
  • define アクション: テンプレート内で名前付きのサブテンプレートを定義するために使用されます。これにより、テンプレートの再利用やモジュール化が可能になります。

Go言語のエラーハンドリング

Go言語では、エラーは戻り値として明示的に扱われます。関数は通常、最後の戻り値として error 型を返します。エラーが発生した場合、error インターフェースを実装する型(通常は error インターフェースを満たす構造体)が返され、呼び出し元はそのエラーをチェックして適切に処理します。

io.Writer インターフェース

io.Writer は、Go言語の標準ライブラリで定義されているインターフェースで、データを書き込むための抽象化を提供します。Write([]byte) (n int, err error) メソッドを実装する任意の型が io.Writer となります。ファイル、ネットワーク接続、メモリバッファなど、様々な出力先に統一された方法でデータを書き込むことができます。

bytes.Buffer

bytes.Buffer は、可変長のバイトバッファを実装する型です。io.Writer インターフェースを実装しており、メモリ上にデータを効率的に書き込むことができます。このコミットでは、エラーメッセージに含める定義済みテンプレート名のリストを一時的に構築するために使用されています。

fmt.Fprintf

fmt.Fprintf は、fmt パッケージの関数で、指定された io.Writer にフォーマットされた文字列を書き込みます。printf スタイルのフォーマット文字列と引数を受け取ります。

技術的詳細

このコミットの主要な変更点は、text/template パッケージの Template.Execute メソッドにおけるエラー報告のロジックにあります。

変更前は、t.Tree == nil || t.Root == nil (つまり、テンプレートがパースされていないか、空である場合) には、単純に %q is an incomplete or empty template というエラーメッセージを返していました。

変更後は、このエラーメッセージに加えて、Template オブジェクト内に定義されている他のテンプレートの名前を列挙するロジックが追加されました。

  1. bytes.Buffer の導入: エラーメッセージに含めるテンプレート名のリストを効率的に構築するために、bytes.Buffer が導入されました。
  2. 定義済みテンプレートの走査: t.tmpl マップ(Template オブジェクトが持つ、名前付きテンプレートのマップ)をイテレートします。
  3. 有効なテンプレートのフィルタリング: 各テンプレート tmpl について、それが Tree == nil || tmpl.Root == nil でない(つまり、有効な内容を持つ)場合にのみ、その名前を bytes.Buffer に追加します。
  4. カンマ区切り: 複数のテンプレート名がある場合、b.Len() > 0 のチェックを行い、カンマとスペース (", ") で区切って連結します。
  5. エラーメッセージの構築: 最終的に bytes.Buffer に格納された文字列を s に代入し、元のエラーメッセージに ; defined templates are: %s という形式で追加します。

また、src/pkg/text/template/template.goTemplate.Name() メソッドにも小さな変更が加えられました。テンプレートに名前が明示的に設定されていない場合(t.name == "")、以前は空文字列が返されていましたが、この変更により "<unnamed>" という文字列を返すようになりました。これは、エラーメッセージなどでテンプレートの名前を表示する際に、より分かりやすい情報を提供するためです。

テストファイル src/pkg/text/template/exec_test.go には、新しいエラーメッセージの動作を検証するためのテストケースが追加されました。

  • 完全に空のテンプレートに対するエラーメッセージ ("empty" is an incomplete or empty template) をテストします。
  • 複数の定義済みテンプレートがある状態で、メインのテンプレートが空の場合のエラーメッセージ ("empty" is an incomplete or empty template; defined templates are: "secondary") をテストします。これにより、定義済みテンプレートの名前が正しくエラーメッセージに含まれることを確認します。

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

src/pkg/text/template/exec.go

--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -5,6 +5,7 @@
 package template
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"reflect"
@@ -125,8 +126,23 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
 		wr:   wr,
 		vars: []variable{{"$", value}},
 	}
+	t.init()
 	if t.Tree == nil || t.Root == nil {
-		state.errorf("%q is an incomplete or empty template", t.name)
+		var b bytes.Buffer
+		for name, tmpl := range t.tmpl {
+			if tmpl.Tree == nil || tmpl.Root == nil {
+				continue
+			}
+			if b.Len() > 0 {
+				b.WriteString(", ")
+			}
+			fmt.Fprintf(&b, "%q", name)
+		}
+		var s string
+		if b.Len() > 0 {
+			s = "; defined templates are: " + b.String()
+		}
+		state.errorf("%q is an incomplete or empty template%s", t.Name(), s)
 	}
 	state.walk(value, t.Root)
 	return

src/pkg/text/template/exec_test.go

--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -816,3 +816,40 @@ func TestExecuteOnNewTemplate(t *testing.T) {
 	// This is issue 3872.
 	_ = New("Name").Templates()
 }
+
+const testTemplates = `{{define "one"}}one{{end}}{{define "two"}}two{{end}}`
+
+func TestMessageForExecuteEmpty(t *testing.T) {
+	// Test a truly empty template.
+	tmpl := New("empty")
+	var b bytes.Buffer
+	err := tmpl.Execute(&b, 0)
+	if err == nil {
+		t.Fatal("expected initial error")
+	}
+	got := err.Error()
+	want := `template: empty: "empty" is an incomplete or empty template`
+	if got != want {
+		t.Errorf("expected error %s got %s", want, got)
+	}
+	// Add a non-empty template to check that the error is helpful.
+	tests, err := New("").Parse(testTemplates)
+	if err != nil {
+		t.Fatal(err)
+	}
+	tmpl.AddParseTree("secondary", tests.Tree)
+	err = tmpl.Execute(&b, 0)
+	if err == nil {
+		t.Fatal("expected second error")
+	}
+	got = err.Error()
+	want = `template: empty: "empty" is an incomplete or empty template; defined templates are: "secondary"`
+	if got != want {
+		t.Errorf("expected error %s got %s", want, got)
+	}
+	// Make sure we can execute the secondary.
+	err = tmpl.ExecuteTemplate(&b, "secondary", 0)
+	if err != nil {
+		t.Fatal(err)
+	}
+}

src/pkg/text/template/template.go

--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -40,6 +40,9 @@ func New(name string) *Template {
 
 // Name returns the name of the template.
 func (t *Template) Name() string {
+	if t.name == "" {
+		return "<unnamed>"
+	}
 	return t.name
 }
 

コアとなるコードの解説

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

  • import "bytes" の追加: bytes.Buffer を使用するために、bytes パッケージがインポートされました。
  • エラーメッセージ生成ロジックの追加:
    • var b bytes.Buffer で、定義済みテンプレート名を収集するための一時バッファを初期化します。
    • for name, tmpl := range t.tmpl ループで、現在の Template オブジェクトが持つすべてのサブテンプレート(t.tmpl マップに格納されている)を反復処理します。
    • if tmpl.Tree == nil || tmpl.Root == nil { continue } は、サブテンプレート自体が空または不完全な場合はスキップし、有効なテンプレートのみを対象とします。
    • if b.Len() > 0 { b.WriteString(", ") } は、最初のテンプレート名以外の場合に、カンマとスペースを追加して整形します。
    • fmt.Fprintf(&b, "%q", name) は、テンプレート名を引用符で囲んでバッファに書き込みます。
    • ループ後、if b.Len() > 0 { s = "; defined templates are: " + b.String() } で、バッファに内容がある場合にのみ、追加のエラーメッセージ部分 s を構築します。
    • 最後に、state.errorf("%q is an incomplete or empty template%s", t.Name(), s) を呼び出し、元のエラーメッセージに s を連結して、より詳細なエラーメッセージを生成します。t.Name() を使用することで、テンプレートの名前が "<unnamed>" の場合でも適切に表示されます。

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

  • testTemplates 定数の追加: 複数の名前付きテンプレートを含む文字列定数が定義され、テストで使用されます。
  • TestMessageForExecuteEmpty 関数の追加:
    • 空のテンプレートのテスト: New("empty") で作成した空のテンプレートを Execute し、期待されるエラーメッセージ ("empty" is an incomplete or empty template) が返されることを確認します。
    • 定義済みテンプレートを含む場合のテスト:
      • New("").Parse(testTemplates)testTemplates をパースし、tests という Template オブジェクトを作成します。
      • tmpl.AddParseTree("secondary", tests.Tree) を使用して、tests のパースツリーを tmpl に "secondary" という名前で追加します。これにより、tmpl は空のメインテンプレートと、"secondary" という名前のサブテンプレートを持つ状態になります。
      • この状態で tmpl.Execute(&b, 0) を呼び出し、期待されるエラーメッセージ ("empty" is an incomplete or empty template; defined templates are: "secondary") が返されることを確認します。これにより、定義済みテンプレートの名前がエラーメッセージに正しく含まれることが検証されます。
    • ExecuteTemplate の確認: 最後に、tmpl.ExecuteTemplate(&b, "secondary", 0) を実行し、"secondary" テンプレートが正しく実行できることを確認します。これは、エラーメッセージの改善が既存の機能に悪影響を与えないことを保証するためです。

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

  • Template.Name() メソッドの変更:
    • if t.name == "" { return "<unnamed>" } という条件が追加されました。これにより、テンプレートに名前が設定されていない場合(New("") などで作成された場合)、Name() メソッドは空文字列ではなく "<unnamed>" を返すようになります。これは、エラーメッセージなどでテンプレートの名前を表示する際に、より分かりやすく、デバッグしやすい情報を提供するための改善です。

これらの変更により、text/template パッケージは、空のテンプレート実行時のエラー報告が大幅に改善され、開発者が問題をより迅速に診断できるようになりました。

関連リンク

参考にした情報源リンク