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

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

このコミットは、Go言語の標準ライブラリである text/template/parse および html/template パッケージにおけるバグ修正と機能改善に関するものです。具体的には、html/template のクローン処理中に parse.Treetext フィールドが適切にコピーされないことによって発生していたパニック(panic)を解消し、テンプレートのクローン機能の堅牢性を向上させています。

コミット

commit eeb758546e10b33be161e76b3c3290dbb7a70a87
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Tue Sep 17 14:19:44 2013 +1000

    text/template/parse, html/template: copy Tree.text during html template clone
    
    The root cause of the panic reported in https://code.google.com/p/go/issues/detail?id=5980
    is that parse's Tree.Text wasn't being copied during the clone.
    
    Fix this by adding and using a Copy method for parse.Tree.
    
    Fixes #5980.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/12420044

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

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

元コミット内容

text/template/parse, html/template: htmlテンプレートのクローン時にTree.textをコピーする

https://code.google.com/p/go/issues/detail?id=5980 で報告されたパニックの根本原因は、parseのTree.Textがクローン時にコピーされていなかったことである。

parse.TreeにCopyメソッドを追加し、それを使用することでこれを修正する。

Fixes #5980.

変更の背景

このコミットは、Go言語のテンプレートエンジンにおける重要なバグを修正するために行われました。具体的には、html/template パッケージでテンプレートをクローン(複製)する際に、内部的に使用される text/template/parse パッケージの Tree 構造体に含まれる text フィールドが適切にコピーされないという問題がありました。

この text フィールドは、テンプレートのパース(解析)時に元のテンプレート文字列を保持するために使用されます。テンプレートのクローンは、既存のテンプレートを基に新しいテンプレートを作成し、その新しいテンプレートに対して追加のパース操作を行ったり、独立した変更を加えたりすることを可能にする機能です。しかし、text フィールドがコピーされないと、クローンされたテンプレートが元のテンプレートと同じ text フィールドを参照し続けることになります。

その結果、クローンされたテンプレートに対して Parse メソッドが呼び出され、新しいテンプレート定義が追加されたり、既存の定義が変更されたりすると、元のテンプレートの text フィールドも意図せず変更されてしまう可能性がありました。これが、https://code.google.com/p/go/issues/detail?id=5980 で報告されたパニックの根本原因でした。パニックは、テンプレートの実行時やさらなるパース操作時に、期待されるテンプレート構造と実際の text フィールドの内容との間に不整合が生じることで発生したと考えられます。

この修正は、テンプレートのクローン機能が期待通りに動作し、元のテンプレートに副作用を与えないようにするために不可欠でした。

前提知識の解説

このコミットを理解するためには、以下のGo言語のテンプレートに関する知識が必要です。

  1. text/templatehtml/template パッケージ:

    • text/template は、任意のテキスト形式の出力を生成するための汎用的なテンプレートエンジンです。
    • html/templatetext/template をベースにしていますが、HTML出力に特化しており、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ機能を提供します。ウェブアプリケーションでHTMLを生成する際には、セキュリティ上の理由から html/template の使用が強く推奨されます。
    • 両パッケージは、テンプレートのパースと実行のコアロジックを共有しています。
  2. テンプレートのパースツリー (parse.Tree):

    • Goのテンプレートエンジンは、テンプレート文字列を解析(パース)して、内部的にツリー構造(抽象構文木、AST)を構築します。このツリーは、テンプレートの構造(アクション、パイプライン、定義など)を表現します。
    • text/template/parse パッケージは、このパースツリーを構築・操作するための低レベルな機能を提供します。
    • parse.Tree 構造体は、テンプレートの名前 (Name)、ルートノード (Root)、そして元のテンプレート文字列 (text) などの情報を含んでいます。text フィールドは、エラーメッセージのコンテキストを提供したり、デバッグ情報として利用されたりする可能性があります。
  3. テンプレートのクローン (Clone メソッド):

    • Template 型(text/template.Template および html/template.Template)には Clone() メソッドが提供されています。
    • このメソッドは、既存のテンプレートオブジェクトのコピーを作成します。これにより、元のテンプレートを変更せずに、そのコピーに対して新しいテンプレート定義を追加したり、既存の定義を上書きしたりすることが可能になります。これは、例えば、ベースとなるレイアウトテンプレートを定義し、それをクローンして各ページ固有のコンテンツを追加するようなシナリオで非常に有用です。
  4. パニック (Panic):

    • Goにおけるパニックは、プログラムの実行中に回復不可能なエラーが発生したことを示すメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、遅延関数(defer)が実行された後、プログラムは終了します。
    • このコミットで修正されたパニックは、テンプレートの内部状態の不整合によって引き起こされたものであり、プログラムのクラッシュにつながる深刻なバグでした。

技術的詳細

このコミットの技術的な核心は、parse.Tree 構造体のディープコピー(深層コピー)を適切に行うことにあります。

元の実装では、html/templateClone メソッドが parse.Tree をクローンする際に、Tree 構造体自体は新しいインスタンスを作成していましたが、その内部フィールドである Root (テンプレートのASTのルートノード) は CopyList() メソッドを使ってディープコピーしていました。しかし、Tree 構造体のもう一つの重要なフィールドである text (元のテンプレート文字列) は、単に参照がコピーされるだけでした。

この「シャローコピー(浅いコピー)」の問題により、クローンされた Tree と元の Tree が同じ text フィールドを共有することになります。もしクローンされたテンプレートに対して Parse メソッドが呼び出され、その過程で text フィールドが変更されるような操作が行われた場合、元のテンプレートの text フィールドも同時に変更されてしまい、結果として元のテンプレートの動作に予期せぬ影響を与える可能性がありました。これが、issue 5980 で報告されたパニックの原因でした。

このコミットでは、この問題を解決するために以下の変更が加えられました。

  1. parse.Tree.Copy() メソッドの追加:

    • src/pkg/text/template/parse/parse.gofunc (t *Tree) Copy() *Tree メソッドが追加されました。
    • このメソッドは、Tree 構造体の新しいインスタンスを作成し、NameParseNameRoot (これは Root.CopyList() を呼び出してディープコピーされる)、そして text フィールドをすべて新しいインスタンスにコピーします。これにより、Tree オブジェクト全体のディープコピーが保証されます。
  2. html/template.Template.Clone() での parse.Tree.Copy() の利用:

    • src/pkg/html/template/template.goTemplate.Clone() メソッド内で、テンプレートの Tree フィールドをクローンする際に、既存の x.Tree = &parse.Tree{...} のような手動でのフィールドコピーではなく、新しく追加された x.Tree = x.Tree.Copy() を呼び出すように変更されました。
    • これにより、html/template のクローン処理が parse.Tree の完全なディープコピーを利用するようになり、text フィールドの共有による問題が解消されました。
  3. テストケースの追加:

    • src/pkg/html/template/clone_test.goTestCloneThenParseTestFuncMapWorksAfterClone という新しいテストケースが追加されました。
      • TestCloneThenParse は、クローンされたテンプレートに新しい定義を追加しても、元のテンプレートに影響がないことを確認します。これは、Tree.text のコピーが正しく行われていることを間接的に検証します。
      • TestFuncMapWorksAfterClone は、issue 5980 の具体的なシナリオを再現し、クローン後に FuncMap が正しく機能し、パニックが発生しないことを確認します。

これらの変更により、html/template のクローン機能はより堅牢になり、元のテンプレートとクローンされたテンプレートが完全に独立して動作することが保証されるようになりました。

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

src/pkg/html/template/template.go

--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -190,12 +190,7 @@ func (t *Template) Clone() (*Template, error) {
 		if src == nil || src.escaped {
 			return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
 		}
-		if x.Tree != nil {
-			x.Tree = &parse.Tree{
-				Name: x.Tree.Name,
-				Root: x.Tree.Root.CopyList(),
-			}
-		}
+		x.Tree = x.Tree.Copy()
 		ret.set[name] = &Template{
 			false,
 			x,
  • Template.Clone() メソッド内で、x.Tree のクローン処理が x.Tree.Copy() を呼び出すように変更されました。これにより、parse.Tree の完全なディープコピーが保証されます。

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

--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -30,6 +30,19 @@ type Tree struct {
 	vars      []string // variables defined at the moment.
 }
 
+// Copy returns a copy of the Tree. Any parsing state is discarded.
+func (t *Tree) Copy() *Tree {
+	if t == nil {
+		return nil
+	}
+	return &Tree{
+		Name:      t.Name,
+		ParseName: t.ParseName,
+		Root:      t.Root.CopyList(),
+		text:      t.text,
+	}
+}
+
 // Parse returns a map from template name to parse.Tree, created by parsing the
 // templates described in the argument string. The top-level template will be
 // given the specified name. If an error is encountered, parsing stops and an
  • Tree 構造体に Copy() メソッドが追加されました。このメソッドは、Tree のすべてのフィールド(Name, ParseName, Root, text)を新しい Tree インスタンスにコピーし、特に RootRoot.CopyList() を使ってディープコピーされます。

src/pkg/html/template/clone_test.go

--- a/src/pkg/html/template/clone_test.go
+++ b/src/pkg/html/template/clone_test.go
@@ -6,6 +6,8 @@ package template
 
  import (
  	"bytes"
+	"errors"
+	"io/ioutil"
  	"testing"
  	"text/template/parse"
  )
@@ -146,3 +148,41 @@ func TestCloneCrash(t *testing.T) {
  	Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`))\n \tt1.Clone()\n }\n+\n+// Ensure that this guarantee from the docs is upheld:\n+// "Further calls to Parse in the copy will add templates\n+// to the copy but not to the original."\n+func TestCloneThenParse(t *testing.T) {\n+\tt0 := Must(New("t0").Parse(`{{define "a"}}{{template "embedded"}}{{end}}`))\n+\tt1 := Must(t0.Clone())\n+\tMust(t1.Parse(`{{define "embedded"}}t1{{end}}`))\n+\tif len(t0.Templates())+1 != len(t1.Templates()) {\n+\t\tt.Error("adding a template to a clone added it to the original")\n+\t}\n+\t// double check that the embedded template isn't available in the original\n+\terr := t0.ExecuteTemplate(ioutil.Discard, "a", nil)\n+\tif err == nil {\n+\t\tt.Error("expected 'no such template' error")\n+\t}\n+}\n+\n+// https://code.google.com/p/go/issues/detail?id=5980\n+func TestFuncMapWorksAfterClone(t *testing.T) {\n+\tfuncs := FuncMap{"customFunc": func() (string, error) {\n+\t\treturn "", errors.New("issue5980")\n+\t}}\n+\n+\t// get the expected error output (no clone)\n+\tuncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))\n+\twantErr := uncloned.Execute(ioutil.Discard, nil)\n+\n+\t// toClone must be the same as uncloned. It has to be recreated from scratch,\n+\t// since cloning cannot occur after execution.\n+\ttoClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))\n+\tcloned := Must(toClone.Clone())\n+\tgotErr := cloned.Execute(ioutil.Discard, nil)\n+\n+\tif wantErr.Error() != gotErr.Error() {\n+\t\tt.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr)\n+\t}\n+}\n```
*   `TestCloneThenParse` と `TestFuncMapWorksAfterClone` の2つの新しいテスト関数が追加され、クローン機能の正確性と `issue 5980` の修正が検証されます。

### `src/pkg/text/template/parse/parse_test.go`

```diff
--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -332,6 +332,22 @@ func TestIsEmpty(t *testing.T) {\n \t}\n }\n \n+func TestErrorContextWithTreeCopy(t *testing.T) {\n+\ttree, err := New("root").Parse("{{if true}}{{end}}", "", "", make(map[string]*Tree), nil)\n+\tif err != nil {\n+\t\tt.Fatalf("unexpected tree parse failure: %v", err)\n+\t}\n+\ttreeCopy := tree.Copy()\n+\twantLocation, wantContext := tree.ErrorContext(tree.Root.Nodes[0])\n+\tgotLocation, gotContext := treeCopy.ErrorContext(treeCopy.Root.Nodes[0])\n+\tif wantLocation != gotLocation {\n+\t\tt.Errorf("wrong error location want %q got %q", wantLocation, gotLocation)\n+\t}\n+\tif wantContext != gotContext {\n+\t\tt.Errorf("wrong error location want %q got %q", wantContext, gotContext)\n+\t}\n+}\n+\n // All failures, and the result is a string that must appear in the error message.\n var errorTests = []parseTest{\n \t// Check line numbers are accurate.\n```
*   `TestErrorContextWithTreeCopy` が追加され、`Tree.Copy()` がエラーコンテキストを正しく保持していることを確認します。

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

このコミットの最も重要な変更は、`parse.Tree` 構造体に `Copy()` メソッドが追加されたことです。

```go
// Copy returns a copy of the Tree. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
	if t == nil {
		return nil
	}
	return &Tree{
		Name:      t.Name,
		ParseName: t.ParseName,
		Root:      t.Root.CopyList(), // ASTのルートノードをディープコピー
		text:      t.text,            // 元のテンプレート文字列をコピー
	}
}

この Copy() メソッドは、Tree オブジェクトの完全な独立したコピーを作成します。特に注目すべきは以下の点です。

  • Root: t.Root.CopyList(),: これは、テンプレートの抽象構文木(AST)のルートノードをディープコピーしている部分です。CopyList() メソッドは、ノードとその子ノードを再帰的に複製し、元のASTへの参照が残らないようにします。
  • text: t.text,: これがこのコミットの主要な修正点です。以前は text フィールドが適切にコピーされていなかったため、クローンされた Tree と元の Tree が同じ text スライスを共有していました。この行により、text フィールドの値(テンプレートのソース文字列)が新しい Tree インスタンスにコピーされ、両者が独立して存在できるようになります。

html/template/template.goClone() メソッドでは、この新しく追加された parse.Tree.Copy() メソッドが利用されるようになりました。

// 変更前:
// if x.Tree != nil {
//     x.Tree = &parse.Tree{
//         Name: x.Tree.Name,
//         Root: x.Tree.Root.CopyList(),
//     }
// }

// 変更後:
x.Tree = x.Tree.Copy()

この変更により、html/templateClone() メソッドが呼び出された際に、内部の parse.Tree オブジェクトが完全に独立したコピーとして生成されることが保証されます。これにより、クローンされたテンプレートに対するその後の操作(例えば、Parse メソッドによる新しいテンプレート定義の追加)が、元のテンプレートの内部状態に影響を与えることがなくなり、issue 5980 で報告されたパニックが解消されました。

この修正は、Goのテンプレートエンジンが提供する「クローン」機能のセマンティクスを正しく実装するために不可欠であり、テンプレートを安全かつ予測可能に再利用するための基盤を強化しました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(上記コミットの差分)
  • Go言語の公式ドキュメント
  • Go言語のIssueトラッカー(issue 5980 の内容)
  • Go Gerrit Code Review
  • Go言語のテンプレートに関する一般的な知識