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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージにおけるバグ修正を含んでいます。具体的には、src/pkg/text/template/multi_test.go に新しいテストケースが追加され、src/pkg/text/template/template.goAddParseTree メソッドに1行の変更が加えられています。

コミット

  • コミットハッシュ: ff006982c355bef55fb7b36944a1cc661bacf287
  • Author: Josh Bleecher Snyder josharian@gmail.com
  • Date: Mon Dec 30 17:17:19 2013 -0800
  • コミットメッセージ:
    text/template: don't panic when using AddParseTree with an unparsed template
    
    Fixes #7032.
    
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/43960045
    

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

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

元コミット内容

text/template: don't panic when using AddParseTree with an unparsed template

このコミットは、text/template パッケージにおいて、パースされていないテンプレートに対して AddParseTree メソッドを使用した場合に発生していたパニック(panic)を修正するものです。

変更の背景

この変更は、Go Issue #7032 に対応するものです。元の実装では、text/template パッケージの Template 型のインスタンスがまだパースされていない状態(つまり、Parse メソッドなどが呼び出されていない状態)で AddParseTree メソッドを呼び出すと、内部で nil ポインタ参照が発生し、プログラムがパニックを起こしていました。

AddParseTree は、既存のテンプレートに別のパース済みテンプレートツリーを追加するためのメソッドです。通常、このメソッドは既に初期化され、場合によってはパース済みの Template インスタンスに対して使用されることを想定しています。しかし、ユーザーが New で作成しただけの、まだ Parse されていない Template インスタンスに対して AddParseTree を呼び出すシナリオが考慮されておらず、その結果として予期せぬパニックが発生していました。

このパニックは、Go言語における「パニックは回復不能なエラーのために予約されている」という原則に反しており、より穏やかなエラーハンドリング(エラーの返却)が望ましい状況でした。このコミットは、この予期せぬパニックを防ぎ、より堅牢な動作を保証することを目的としています。

前提知識の解説

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

text/template パッケージは、Go言語でテキストベースの出力を生成するためのデータ駆動型テンプレートエンジンを提供します。HTML、XML、プレーンテキストなど、様々な形式のテキストを生成するのに使用されます。

  • Template: テンプレートのコンパイル済み表現を表す構造体です。
  • New(name string) *Template: 指定された名前で新しい空の Template インスタンスを作成します。この時点では、テンプレートはまだパースされていません。
  • Parse(text string) (*Template, error): テンプレート文字列をパースし、Template インスタンスにコンパイルします。
  • AddParseTree(name string, tree *parse.Tree) (*Template, error): 既存の Template インスタンスに、指定された名前とパースツリー (*parse.Tree) を持つ新しいテンプレートを追加します。これにより、複数のテンプレートを一つの Template インスタンス内で管理し、{{template "name"}} のように相互に参照できるようになります。

panicnil ポインタ参照

Go言語における panic は、プログラムの実行を停止させる回復不能なエラーを示します。これは通常、プログラマーの論理的な誤りや、予期せぬ深刻な状態(例: メモリ不足)が発生した場合に用いられます。

nil ポインタ参照は、nil 値を持つポインタが指すメモリ領域にアクセスしようとしたときに発生するランタイムエラーです。これはGo言語で最も一般的なパニックの原因の一つであり、プログラムのクラッシュにつながります。

このコミットの文脈では、Template インスタンスが適切に初期化されていない状態で内部フィールドにアクセスしようとした結果、nil ポインタ参照によるパニックが発生していました。

技術的詳細

問題の根源は、Template 型の内部構造にあります。Template 型は、テンプレートの共通状態を保持する common フィールド(*common 型)と、名前でテンプレートをマップする tmpl フィールド(map[string]*Template 型)を持っています。

New 関数で Template インスタンスを作成した直後、common フィールドは nil のままです。Parse メソッドが呼び出されると、common フィールドが適切に初期化されます。

AddParseTree メソッドの元の実装では、テンプレート名の再定義をチェックするために t.tmpl[name] != nil という条件を使用していました。しかし、t.tmplt.common が初期化された後にのみ有効なマップとして機能します。New で作成されたばかりの Template インスタンスでは、t.commonnil であるため、t.tmpl も実質的に nil と同等な状態です。この状態で t.tmpl[name] のようなマップアクセスを行うと、nil マップへのアクセスとなり、ランタイムパニックが発生していました。

このコミットの修正は、t.tmpl[name] != nil のチェックの前に、t.common != nil というガード条件を追加することで、この問題を解決しています。これにより、common フィールドが初期化されていない場合には、t.tmpl へのアクセスを避け、パニックを防ぐことができます。

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

diff --git a/src/pkg/text/template/multi_test.go b/src/pkg/text/template/multi_test.go
index 1f6ed5d8e2..e4e804880a 100644
--- a/src/pkg/text/template/multi_test.go
+++ b/src/pkg/text/template/multi_test.go
@@ -259,6 +259,18 @@ func TestAddParseTree(t *testing.T) {
 	}\n }\n \n+// Issue 7032
+func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
+	master := "{{define \"master\"}}{{end}}"\n+	tmpl := New("master")\n+	tree, err := parse.Parse("master", master, "", "", nil)\n+	if err != nil {\n+		t.Fatalf("unexpected parse err: %v", err)\n+	}\n+	masterTree := tree["master"]\n+	tmpl.AddParseTree("master", masterTree) // used to panic\n+}\n+\n func TestRedefinition(t *testing.T) {
 	var tmpl *Template
 	var err error
 diff --git a/src/pkg/text/template/template.go b/src/pkg/text/template/template.go
index a2b9062ad1..249d0cbfb9 100644
--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -105,7 +105,7 @@ func (t *Template) copy(c *common) *Template {
 // AddParseTree creates a new template with the name and parse tree
 // and associates it with t.
 func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
-\tif t.tmpl[name] != nil {\n+\tif t.common != nil && t.tmpl[name] != nil {\n \t\treturn nil, fmt.Errorf("template: redefinition of template %q", name)\n \t}\n \tnt := t.New(name)\n```

## コアとなるコードの解説

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

```go
 func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
-	if t.tmpl[name] != nil {
+	if t.common != nil && t.tmpl[name] != nil {
 		return nil, fmt.Errorf("template: redefinition of template %q", name)
 	}
 	nt := t.New(name)

この変更は、AddParseTree メソッドの冒頭にあるテンプレート名の再定義チェックの条件を修正しています。

  • 変更前: if t.tmpl[name] != nil { ... }
    • t.tmplTemplate インスタンスが New で作成された直後には、その内部の common フィールドが nil であるため、nil マップとして扱われます。nil マップに対して要素アクセス(t.tmpl[name])を行うと、Goランタイムはパニックを発生させます。
  • 変更後: if t.common != nil && t.tmpl[name] != nil { ... }
    • t.common != nil という条件が追加されました。これにより、t.commonnil でない場合にのみ、t.tmpl[name] のチェックが実行されます。
    • Go言語の論理AND (&&) 演算子はショートサーキット評価を行うため、t.commonnil であれば、t.tmpl[name] の評価は行われず、nil マップへのアクセスによるパニックが回避されます。
    • この修正により、パースされていない(commonnil の)Template インスタンスに対して AddParseTree が呼び出されても、パニックすることなく処理が続行され、新しいテンプレートが追加されるようになります。

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

// Issue 7032
func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
	master := "{{define \"master\"}}{{end}}"
	tmpl := New("master")
	tree, err := parse.Parse("master", master, "", "", nil)
	if err != nil {
		t.Fatalf("unexpected parse err: %v", err)
	}
	masterTree := tree["master"]
	tmpl.AddParseTree("master", masterTree) // used to panic
}

このテストケースは、Issue #7032 で報告されたパニックを再現し、修正が正しく機能することを確認するために追加されました。

  1. master := "{{define \"master\"}}{{end}}": master という名前の定義済みテンプレートを含む文字列を定義します。
  2. tmpl := New("master"): New 関数を使って、まだパースされていない新しい Template インスタンス tmpl を作成します。この時点では、tmpl の内部 common フィールドは nil です。
  3. tree, err := parse.Parse("master", master, "", "", nil): parse パッケージを使用して、master テンプレート文字列をパースし、そのパースツリーを取得します。
  4. masterTree := tree["master"]: パースツリーから master テンプレートに対応するツリーを取得します。
  5. tmpl.AddParseTree("master", masterTree): ここが重要なポイントです。まだパースされていない tmpl インスタンスに対して、パース済みの masterTreeAddParseTree で追加しようとします。
    • 修正前: この行で nil マップへのアクセスが発生し、パニックしていました。
    • 修正後: t.common != nil のチェックによりパニックが回避され、テストは正常に完了します。

このテストケースは、修正が意図した通りにパニックを抑制し、AddParseTree がパースされていないテンプレートに対しても安全に呼び出せるようになったことを検証しています。

関連リンク

参考にした情報源リンク