[インデックス 16942] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージ内の parse
サブパッケージにおけるテキストノードの出力形式に関する変更です。具体的には、TextNode
の String()
メソッドがテキストをどのようにフォーマットするかを修正し、それに関連するテストファイルを更新しています。
変更されたファイルは以下の通りです。
src/pkg/text/template/multi_test.go
:text/template
パッケージのマルチテンプレート解析に関するテストファイル。TextNode
の出力形式変更に伴い、期待される出力文字列が修正されています。src/pkg/text/template/parse/node.go
:text/template/parse
パッケージにおけるノード構造の定義ファイル。TextNode
のString()
メソッドの実装が変更されています。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テンプレートの有効な入力として再利用できるようにすることです。
以前の実装では、TextNode
の String()
メソッドは fmt.Sprintf("%q", t.Text)
を使用してテキストを出力していました。%q
フォーマット動詞は、文字列をGoの構文で引用符で囲み、特殊文字をエスケープして出力します。例えば、FOO
というテキストは \" FOO \"
のように出力されます。
この引用符で囲まれた形式は、デバッグやログ出力には便利ですが、Goテンプレートのパーサーが期待する生のテキストとは異なります。そのため、TextNode
を文字列として出力し、その出力を再度テンプレートエンジンに渡して解析しようとすると、構文エラーが発生する可能性がありました。
コミットメッセージにある Fixes #4593
は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。ただし、現在の検索では golang/go
リポジトリの該当Issueが見つからず、grpc/grpc-go
のIssueがヒットするため、Issue番号が異なるか、またはIssueが移動・クローズされた可能性があります。しかし、コミットメッセージ自体が問題の核心を明確に説明しています。
この変更により、TextNode
の String()
メソッドは fmt.Sprintf("%s", t.Text)
を使用するようになります。%s
フォーマット動詞は、文字列を引用符なしでそのまま出力します。これにより、FOO
は FOO
と出力され、Goテンプレートの有効な入力として直接使用できるようになります。
結果として、テンプレートの解析ツリーを文字列として「プリント」し、その文字列を再度解析するような操作(例えば、テンプレートの正規化や最適化のプロセス)が可能になり、より堅牢なテンプレート処理システムを構築するための基盤が提供されます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。
-
text/template
パッケージ: Go言語の標準ライブラリの一つで、テキストベースのテンプレートを生成するための機能を提供します。HTML、XML、プレーンテキストなど、様々な形式のテキストを動的に生成するのに使用されます。テンプレートは、プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストと、Goのデータ構造を組み合わせて最終的な出力を生成します。 -
text/template/parse
サブパッケージ:text/template
パッケージの内部で使用されるサブパッケージで、テンプレート文字列を解析し、抽象構文木(AST: Abstract Syntax Tree)を構築する役割を担います。このASTは、テンプレートの構造をGoのデータ構造として表現したもので、テンプレートエンジンが実行時にテンプレートを処理するために使用します。 -
抽象構文木 (AST): プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。
text/template/parse
パッケージでは、テンプレート文字列を解析して、様々な種類の「ノード」(Node)からなるASTを生成します。 -
Node
インターフェース:text/template/parse
パッケージで定義されているインターフェースで、AST内のすべての要素(ノード)が満たすべき共通の振る舞いを定義します。例えば、String()
メソッドはノードの文字列表現を返します。 -
TextNode
構造体:Node
インターフェースを実装する具体的なノードの一つです。テンプレート内の静的なテキスト部分(つまり、アクションブロック{{...}}
の外側にある通常のテキスト)を表します。例えば、Hello, {{.Name}}!
というテンプレートでは、Hello,
と!
がそれぞれTextNode
として解析されます。 -
fmt
パッケージとフォーマット動詞 (%s
,%q
): Go言語の標準ライブラリで、フォーマットされたI/O(入出力)を提供します。fmt.Sprintf
関数は、指定されたフォーマット文字列と引数を使用して文字列を生成します。%s
: 文字列をそのまま出力するためのフォーマット動詞です。引用符やエスケープは行われません。%q
: 文字列をGoの構文で引用符で囲み、特殊文字をエスケープして出力するためのフォーマット動詞です。例えば、改行文字\n
は\\n
とエスケープされ、全体が二重引用符で囲まれます。
これらの概念を理解することで、TextNode
の String()
メソッドがどのように動作し、なぜ %q
から %s
への変更が重要であるかが明確になります。
技術的詳細
このコミットの技術的な核心は、text/template/parse
パッケージにおける TextNode
の文字列表現の変更と、それに伴うテストの調整です。
TextNode.String()
メソッドの変更
以前の src/pkg/text/template/parse/node.go
では、TextNode
の String()
メソッドは以下のように実装されていました。
func (t *TextNode) String() string {
return fmt.Sprintf("%q", t.Text)
}
この実装では、fmt.Sprintf
の %q
フォーマット動詞が使用されていました。%q
は、文字列をGoのソースコードリテラル形式で表現します。これは、文字列を二重引用符で囲み、内部の特殊文字(例: \n
, \t
, "
など)をエスケープします。
例えば、TextNode
が FOO
というテキストを含んでいた場合、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.」とあります。これは、テスト時には TextNode
の String()
メソッドが %q
フォーマットで出力するように一時的に変更されることを意味します。
src/pkg/text/template/parse/parse_test.go
の testParse
関数に以下の変更が加えられました。
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"
の追加:TextNode
のString()
メソッドが使用するフォーマット文字列を保持するためのグローバル変数textFormat
が導入されました。デフォルト値は"%s"
に設定されており、これは文字列を引用符なしでそのまま出力することを意味します。コメントには「Changed to "%q" in tests for better error messages.」とあり、テスト時にはこの変数が一時的に"%q"
に変更されることが示唆されています。 -
func (t *TextNode) String() string
の変更:TextNode
のString()
メソッドの実装が、fmt.Sprintf("%q", t.Text)
からfmt.Sprintf(textFormat, t.Text)
に変更されました。これにより、TextNode
の文字列表現は、textFormat
変数の現在の値(通常は%s
)に従ってフォーマットされるようになります。この変更が、テンプレートのテキストノードがGoテンプレートの有効な入力として再利用可能になる主要な部分です。
src/pkg/text/template/multi_test.go
の変更
multiParseTests
の期待値の修正:multiParseTests
というテストデータ内の期待される出力文字列が修正されました。以前はTextNode
のString()
メソッドが%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 Change-list: https://golang.org/cl/12006047
参考にした情報源リンク
- 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