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

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

このコミットは、Go言語の標準ライブラリである text/template パッケージ内の parse サブパッケージにおけるテキストノードの出力形式に関する変更です。具体的には、TextNodeString() メソッドがテキストをどのようにフォーマットするかを修正し、それに関連するテストファイルを更新しています。

変更されたファイルは以下の通りです。

  • src/pkg/text/template/multi_test.go: text/template パッケージのマルチテンプレート解析に関するテストファイル。TextNode の出力形式変更に伴い、期待される出力文字列が修正されています。
  • src/pkg/text/template/parse/node.go: text/template/parse パッケージにおけるノード構造の定義ファイル。TextNodeString() メソッドの実装が変更されています。
  • src/pkg/text/template/parse/parse_test.go: text/template/parse パッケージの解析に関するテストファイル。テスト中に TextNode の出力形式を一時的に変更するためのコードが追加されています。

コミット

commit df4de948a5145519411696b4f741bcdd6480bece
Author: Rob Pike <r@golang.org>
Date:   Wed Jul 31 15:09:13 2013 +1000

    text/template/parse: print TextNodes using %s not %q
    This means that printing a Node will produce output that can be used as valid input.
    It won't be exactly the same - some spacing may be different - but it will mean the same.
    
    Fixes #4593.
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/12006047

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

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

元コミット内容

text/template/parse: print TextNodes using %s not %q
This means that printing a Node will produce output that can be used as valid input.
It won't be exactly the same - some spacing may be different - but it will mean the same.

Fixes #4593.

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/12006047

変更の背景

このコミットの主な目的は、text/template パッケージの解析ツリー内でテキストを表す TextNode が文字列として出力される際に、その出力がGoテンプレートの有効な入力として再利用できるようにすることです。

以前の実装では、TextNodeString() メソッドは fmt.Sprintf("%q", t.Text) を使用してテキストを出力していました。%q フォーマット動詞は、文字列をGoの構文で引用符で囲み、特殊文字をエスケープして出力します。例えば、FOO というテキストは \" FOO \" のように出力されます。

この引用符で囲まれた形式は、デバッグやログ出力には便利ですが、Goテンプレートのパーサーが期待する生のテキストとは異なります。そのため、TextNode を文字列として出力し、その出力を再度テンプレートエンジンに渡して解析しようとすると、構文エラーが発生する可能性がありました。

コミットメッセージにある Fixes #4593 は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。ただし、現在の検索では golang/go リポジトリの該当Issueが見つからず、grpc/grpc-go のIssueがヒットするため、Issue番号が異なるか、またはIssueが移動・クローズされた可能性があります。しかし、コミットメッセージ自体が問題の核心を明確に説明しています。

この変更により、TextNodeString() メソッドは fmt.Sprintf("%s", t.Text) を使用するようになります。%s フォーマット動詞は、文字列を引用符なしでそのまま出力します。これにより、FOOFOO と出力され、Goテンプレートの有効な入力として直接使用できるようになります。

結果として、テンプレートの解析ツリーを文字列として「プリント」し、その文字列を再度解析するような操作(例えば、テンプレートの正規化や最適化のプロセス)が可能になり、より堅牢なテンプレート処理システムを構築するための基盤が提供されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

  1. text/template パッケージ: Go言語の標準ライブラリの一つで、テキストベースのテンプレートを生成するための機能を提供します。HTML、XML、プレーンテキストなど、様々な形式のテキストを動的に生成するのに使用されます。テンプレートは、プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストと、Goのデータ構造を組み合わせて最終的な出力を生成します。

  2. text/template/parse サブパッケージ: text/template パッケージの内部で使用されるサブパッケージで、テンプレート文字列を解析し、抽象構文木(AST: Abstract Syntax Tree)を構築する役割を担います。このASTは、テンプレートの構造をGoのデータ構造として表現したもので、テンプレートエンジンが実行時にテンプレートを処理するために使用します。

  3. 抽象構文木 (AST): プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。text/template/parse パッケージでは、テンプレート文字列を解析して、様々な種類の「ノード」(Node)からなるASTを生成します。

  4. Node インターフェース: text/template/parse パッケージで定義されているインターフェースで、AST内のすべての要素(ノード)が満たすべき共通の振る舞いを定義します。例えば、String() メソッドはノードの文字列表現を返します。

  5. TextNode 構造体: Node インターフェースを実装する具体的なノードの一つです。テンプレート内の静的なテキスト部分(つまり、アクションブロック {{...}} の外側にある通常のテキスト)を表します。例えば、Hello, {{.Name}}! というテンプレートでは、Hello, ! がそれぞれ TextNode として解析されます。

  6. fmt パッケージとフォーマット動詞 (%s, %q): Go言語の標準ライブラリで、フォーマットされたI/O(入出力)を提供します。fmt.Sprintf 関数は、指定されたフォーマット文字列と引数を使用して文字列を生成します。

    • %s: 文字列をそのまま出力するためのフォーマット動詞です。引用符やエスケープは行われません。
    • %q: 文字列をGoの構文で引用符で囲み、特殊文字をエスケープして出力するためのフォーマット動詞です。例えば、改行文字 \n\\n とエスケープされ、全体が二重引用符で囲まれます。

これらの概念を理解することで、TextNodeString() メソッドがどのように動作し、なぜ %q から %s への変更が重要であるかが明確になります。

技術的詳細

このコミットの技術的な核心は、text/template/parse パッケージにおける TextNode の文字列表現の変更と、それに伴うテストの調整です。

TextNode.String() メソッドの変更

以前の src/pkg/text/template/parse/node.go では、TextNodeString() メソッドは以下のように実装されていました。

func (t *TextNode) String() string {
	return fmt.Sprintf("%q", t.Text)
}

この実装では、fmt.Sprintf%q フォーマット動詞が使用されていました。%q は、文字列をGoのソースコードリテラル形式で表現します。これは、文字列を二重引用符で囲み、内部の特殊文字(例: \n, \t, " など)をエスケープします。

例えば、TextNodeFOO というテキストを含んでいた場合、String() メソッドは \" FOO \" という文字列を返していました。この形式は、デバッグ出力や、Goのソースコードに直接埋め込む場合には適切ですが、Goテンプレートのパーサーが期待する生のテキスト形式とは異なります。Goテンプレートのパーサーは、{{define "foo"}} FOO {{end}}FOO の部分を、引用符なしのそのままのテキストとして解釈します。

このコミットでは、String() メソッドの実装が以下のように変更されました。

var textFormat = "%s" // Changed to "%q" in tests for better error messages.

func (t *TextNode) String() string {
	return fmt.Sprintf(textFormat, t.Text)
}

新しい実装では、fmt.Sprintf のフォーマット文字列が %q から textFormat という変数に置き換えられました。そして、この textFormat 変数のデフォルト値は "%s" に設定されています。%s フォーマット動詞は、文字列を引用符なしでそのまま出力します。これにより、FOO というテキストは FOO と出力されるようになり、Goテンプレートの有効な入力として直接使用できるようになります。

テストにおける textFormat の利用

textFormat 変数が導入された理由として、コメントに「Changed to "%q" in tests for better error messages.」とあります。これは、テスト時には TextNodeString() メソッドが %q フォーマットで出力するように一時的に変更されることを意味します。

src/pkg/text/template/parse/parse_test.gotestParse 関数に以下の変更が加えられました。

func testParse(doCopy bool, t *testing.T) {
	textFormat = "%q"
	defer func() { textFormat = "%s" }()
	for _, test := range parseTests {
		// ...
	}
}

このコードは、testParse 関数が実行される際に、グローバル変数 textFormat を一時的に "%q" に設定します。そして、defer ステートメントを使用して、関数が終了する際に textFormat を元の "%s" に戻すようにしています。

なぜテストで %q を使用するのかというと、テストの目的は通常、期待される出力と実際に出力された文字列を比較することです。%q フォーマットは、文字列内の非表示文字(スペース、タブ、改行など)や特殊文字を明確に表現するため、テストの失敗時にどの文字が異なっているのかを視覚的に把握しやすくなります。例えば、余分なスペースがある場合、"foo" "foo " の違いは %q で出力すると ""foo"""" foo "" のように明確になり、デバッグが容易になります。

テストケースの更新

src/pkg/text/template/multi_test.go では、multiParseTests というテストデータが更新されています。以前は TextNode の文字列表現が %q で出力されることを期待していたため、期待される結果も引用符で囲まれていました。

// 変更前
[]string{`" FOO "`}},
[]string{`" FOO "`, `" BAR "`}},

// 変更後
[]string{" FOO "}},
[]string{" FOO ", " BAR "}},

この変更は、TextNode.String() メソッドが %s で出力するようになったことに合わせて、テストの期待値を修正したものです。これにより、テストが新しい動作と一致するようになります。

まとめ

このコミットは、text/template パッケージの内部的な整合性を高め、テンプレートのASTを文字列として出力した際に、その出力がGoテンプレートの有効な入力として再利用できるようにすることで、より柔軟なテンプレート処理を可能にしています。また、テスト時にはデバッグのしやすさを考慮して、一時的に異なるフォーマットを使用するという巧妙な設計がなされています。

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

src/pkg/text/template/multi_test.go

--- a/src/pkg/text/template/multi_test.go
+++ b/src/pkg/text/template/multi_test.go
@@ -33,10 +33,10 @@ var multiParseTests = []multiParseTest{
 		nil},\n \t{\"one\", `{{define \"foo\"}} FOO {{end}}`, noError,\n \t\t[]string{\"foo\"},\n-\t\t[]string{`\" FOO \"`}},\n+\t\t[]string{\" FOO \"}},\n \t{\"two\", `{{define \"foo\"}} FOO {{end}}{{define \"bar\"}} BAR {{end}}`, noError,\n \t\t[]string{\"foo\", \"bar\"},\n-\t\t[]string{`\" FOO \"`, `\" BAR \"`}},\n+\t\t[]string{\" FOO \", \" BAR \"}},\n \t// errors\n \t{\"missing end\", `{{define \"foo\"}} FOO `, hasError,\n \t\tnil,\n```

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

```diff
--- a/src/pkg/text/template/parse/node.go
+++ b/src/pkg/text/template/parse/node.go
@@ -13,6 +13,8 @@ import (\n 	\"strings\"\n )\n \n+var textFormat = "%s" // Changed to "%q" in tests for better error messages.\n+\n // A Node is an element in the parse tree. The interface is trivial.\n // The interface contains an unexported method so that only\n // types local to this package can satisfy it.\n@@ -125,7 +127,7 @@ func newText(pos Pos, text string) *TextNode {\n }\n \n func (t *TextNode) String() string {\n-\treturn fmt.Sprintf(\"%q\", t.Text)\n+\treturn fmt.Sprintf(textFormat, t.Text)\n }\n \n func (t *TextNode) Copy() Node {\

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

--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -256,6 +256,8 @@ var builtins = map[string]interface{}{\n }\n \n func testParse(doCopy bool, t *testing.T) {\n+\ttextFormat = "%q"\n+\tdefer func() { textFormat = "%s" }()\n \tfor _, test := range parseTests {\n \t\ttmpl, err := New(test.name).Parse(test.input, \"\", \"\", make(map[string]*Tree), builtins)\n \t\tswitch {\

コアとなるコードの解説

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

  • var textFormat = "%s" の追加: TextNodeString() メソッドが使用するフォーマット文字列を保持するためのグローバル変数 textFormat が導入されました。デフォルト値は "%s" に設定されており、これは文字列を引用符なしでそのまま出力することを意味します。コメントには「Changed to "%q" in tests for better error messages.」とあり、テスト時にはこの変数が一時的に "%q" に変更されることが示唆されています。

  • func (t *TextNode) String() string の変更: TextNodeString() メソッドの実装が、fmt.Sprintf("%q", t.Text) から fmt.Sprintf(textFormat, t.Text) に変更されました。これにより、TextNode の文字列表現は、textFormat 変数の現在の値(通常は %s)に従ってフォーマットされるようになります。この変更が、テンプレートのテキストノードがGoテンプレートの有効な入力として再利用可能になる主要な部分です。

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

  • multiParseTests の期待値の修正: multiParseTests というテストデータ内の期待される出力文字列が修正されました。以前は TextNodeString() メソッドが %q で出力することを期待していたため、期待値も " で囲まれていました(例: \" FOO \")。この変更により、TextNode.String()%s で出力するようになったため、期待値も引用符なしの生文字列(例: " FOO ")に修正されています。これは、コードの動作変更に合わせてテストの期待値を同期させるためのものです。

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

  • testParse 関数内での textFormat の一時的な変更: testParse 関数は、テンプレートの解析テストを実行するためのヘルパー関数です。この関数内で、textFormat = "%q" が設定され、defer func() { textFormat = "%s" }() によって関数終了時に textFormat が元の "%s" に戻されるように設定されています。 このメカニズムにより、parse_test.go 内のテストが実行されている間は、TextNode.String() メソッドが %q フォーマットで文字列を出力するようになります。これにより、テストの失敗時に、文字列の差異が引用符付きで明確に表示され、デバッグが容易になります。テストが完了すると、textFormat は元の %s に戻され、他の部分での通常の動作に影響を与えません。

これらの変更は、text/template パッケージの内部的な整合性を高め、テンプレートのASTを文字列として出力した際に、その出力がGoテンプレートの有効な入力として再利用できるようにするというコミットの目的を達成しています。また、テストのデバッグ容易性を損なわないようにするための工夫も施されています。

関連リンク

参考にした情報源リンク

  • Go言語 text/template パッケージ公式ドキュメント: https://pkg.go.dev/text/template
  • Go言語 fmt パッケージ公式ドキュメント: https://pkg.go.dev/fmt
  • Go言語のIssueトラッカー (一般的な情報源として): https://github.com/golang/go/issues
  • Go言語のソースコード (変更されたファイル):
    • src/pkg/text/template/multi_test.go
    • src/pkg/text/template/parse/node.go
    • src/pkg/text/template/parse/parse_test.go