[インデックス 11271] ファイルの概要
このコミットは、Go言語の text/template
および html/template
パッケージにおけるテンプレートのパースツリーノードの文字列表現を改善することを目的としています。具体的には、デバッグ用途で内部的なパースツリー構造を出力していた node.String()
メソッド群の挙動を、より人間が読みやすい標準的なテンプレート構文で出力するように変更しています。これにより、エラーメッセージの可読性が向上し、より自然なエラー報告が可能になります。
コミット
commit c837e612bd449cd7298ce925749b9f09b54fea48
Author: Rob Pike <r@golang.org>
Date: Thu Jan 19 13:51:37 2012 -0800
text/template/parse: use human error prints
The previous version of all the node.String methods printed the parse
tree and was useful for developing the parse tree code. Now that that's done,
we might as well print the nodes using the standard template syntax.
It's much easier to read and makes error reporting look more natural.
Helps issue 2644.
R=rsc, n13m3y3r
CC=golang-dev
https://golang.org/cl/5553066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c837e612bd449cd7298ce925749b9f09b54fea48
元コミット内容
text/template/parse: use human error prints
以前の node.String
メソッドのバージョンは、パースツリーをそのまま出力しており、パースツリーコードの開発には役立っていました。しかし、その開発が完了した現在、ノードを標準的なテンプレート構文で出力するように変更しました。これにより、読みやすさが大幅に向上し、エラー報告がより自然に見えるようになります。
Issue 2644の解決に貢献します。
変更の背景
この変更の背景には、Go言語のテンプレートパッケージが成熟し、開発段階から実運用段階へと移行する過程があります。初期の開発段階では、テンプレートのパース(構文解析)処理が正しく機能しているかを確認するために、内部的なパースツリーの構造をそのまま出力する String()
メソッドが非常に有用でした。これにより、開発者はテンプレートがどのように解析され、どのような抽象構文木(AST)が構築されているかを詳細に把握し、デバッグや機能追加を行うことができました。
しかし、パース処理の開発が一段落し、パッケージが安定してきた段階で、その String()
メソッドの出力がユーザーにとって分かりにくいという問題が浮上しました。特に、テンプレートの構文エラーが発生した際、エラーメッセージに内部的なパースツリーの表現が含まれていると、ユーザーはその意味を理解するのが困難でした。ユーザーは、テンプレートの構文そのものに慣れているため、エラーメッセージもテンプレート構文に即した形式で表示される方が、問題の特定と修正が容易になります。
コミットメッセージに記載されている "Helps issue 2644" は、この変更がGoのIssueトラッカーで報告された特定の課題(Issue 2644: text/template: improve error messages
)の解決に貢献することを示しています。このIssueでは、テンプレートのエラーメッセージがユーザーフレンドリーではないという点が指摘されており、特にパースツリーの内部表現がそのまま出力されることが問題視されていました。
このコミットは、開発者向けのデバッグ出力から、ユーザー向けの分かりやすいエラー報告へと焦点を移すことで、Goのテンプレートパッケージのユーザビリティを向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Go言語の
text/template
およびhtml/template
パッケージ:- これらはGo言語に組み込まれているテンプレートエンジンです。WebアプリケーションのHTML生成や、テキストベースの出力(設定ファイル、コード生成など)に利用されます。
text/template
は汎用的なテキスト出力に、html/template
はHTMLの自動エスケープ機能を提供し、クロスサイトスクリプティング(XSS)攻撃を防ぐためのセキュリティ対策が施されています。- テンプレートは、
{{...}}
で囲まれたアクション(変数、関数呼び出し、制御構造など)と、それ以外の静的なテキストで構成されます。
-
構文解析(Parsing)と抽象構文木(Abstract Syntax Tree: AST):
- 構文解析: プログラミング言語やテンプレート言語のソースコード(この場合はテンプレート文字列)を読み込み、その文法構造を解析するプロセスです。字句解析(Lexical Analysis)によってトークン列に変換された後、構文解析器(Parser)がそのトークン列から文法規則に従って構造を構築します。
- 抽象構文木(AST): 構文解析の結果として生成されるツリー構造のデータ表現です。ソースコードの抽象的な構文構造を表現し、コンパイラやインタプリタがコードの意味を理解し、処理を行うための基盤となります。各ノードは、変数、演算子、関数呼び出し、制御構造などの言語要素に対応します。
- Goのテンプレートパッケージでは、入力されたテンプレート文字列がパースされ、内部的に
parse
パッケージで定義されたNode
インターフェースを実装する様々なノード(ActionNode
,TextNode
,PipeNode
など)からなるASTが構築されます。
-
String()
メソッドの役割:- Go言語において、型が
String() string
メソッドを実装している場合、その型の値がfmt.Print()
,fmt.Println()
,fmt.Sprintf()
などの関数で出力される際に、このメソッドが自動的に呼び出され、その戻り値が文字列として使用されます。 - 通常、
String()
メソッドは、その型の値を人間が読みやすい形式で表現するために使用されます。デバッグ時には、内部状態を詳細に表示するために使われることもありますが、一般的には簡潔で分かりやすい表現が求められます。
- Go言語において、型が
-
エラー報告の重要性:
- ソフトウェアにおいて、エラーメッセージはユーザーが問題を理解し、解決するための重要な手がかりです。
- 特に構文エラーのようなユーザーの入力ミスに起因するエラーの場合、エラーメッセージが具体的で、問題箇所を特定しやすく、かつユーザーが慣れ親しんだ形式で表示されることが、ユーザビリティの向上に直結します。内部的なデバッグ情報がそのまま表示されると、ユーザーは混乱し、問題解決に時間がかかります。
このコミットは、GoテンプレートのASTノードが持つ String()
メソッドの出力を、内部的なAST表現から、ユーザーが記述するテンプレート構文に近い形式へと変更することで、エラーメッセージの質を向上させています。
技術的詳細
このコミットの技術的な核心は、src/pkg/text/template/parse/node.go
ファイル内の様々なノード型に実装されている String()
メソッドの変更にあります。以前のバージョンでは、これらのメソッドは主にパースツリーのデバッグを目的として、ノードの内部構造(例: (action: [(command: [F=[X]])])
)を詳細に文字列化していました。しかし、このコミットでは、その出力をGoテンプレートの実際の構文(例: {{.X}}
)に近づけるように修正されています。
具体的な変更点は以下の通りです。
-
ListNode
:- 以前は
[(text: " FOO ")]
のように角括弧で囲まれていましたが、これらが削除され、単にノードの文字列が連結されるようになりました。これにより、リスト内の要素がより自然に連続して表示されます。
- 以前は
-
TextNode
:- 以前は
(text: "some text")
のように、テキストであることを示すプレフィックスと括弧が付加されていましたが、変更後は単に"
some text"
のように、引用符で囲まれたテキストそのものが出力されるようになりました。
- 以前は
-
PipeNode
:- パイプライン(
|
)や変数宣言(:=
)を含む複雑なノードです。以前は[(command: [F=[X]]) (command: [I=html])]
のように内部構造が露出していましたが、変更後は{{.X | html}}
のように、実際のテンプレート構文に近い形式で出力されるようになりました。変数宣言も{{$x := .X | .Y}}
のように表現されます。
- パイプライン(
-
ActionNode
:{{...}}
で囲まれたアクションを表すノードです。以前は(action: %v)
のように内部のパイプノードをラップしていましたが、変更後は{{%s}}
の形式で、内部のパイプノードの文字列を直接埋め込むようになりました。これにより、{{.X}}
のような出力が得られます。
-
CommandNode
:- コマンド(関数呼び出しやパイプラインの要素)を表します。以前は
(command: %v)
のように引数をリスト形式で出力していましたが、変更後はprintf %q 23
のように、引数がスペース区切りで連結されるようになりました。
- コマンド(関数呼び出しやパイプラインの要素)を表します。以前は
-
IdentifierNode
:- 識別子(関数名など)を表します。以前は
I=printf
のようにプレフィックスが付いていましたが、変更後は単にprintf
のように識別子そのものが出力されます。
- 識別子(関数名など)を表します。以前は
-
VariableNode
:- 変数(
$x
など)を表します。以前はV=[$x]
のようにプレフィックスと角括弧が付いていましたが、変更後は$x
のように変数名そのものが出力されます。フィールドアクセス($.I
)も$.I
のようにドット区切りで表現されます。
- 変数(
-
DotNode
:- 現在のコンテキストを表すドット(
.
)です。以前は{{<.>}}
のように特殊な形式でしたが、変更後は単に.
と出力されます。
- 現在のコンテキストを表すドット(
-
FieldNode
:- フィールドアクセス(
.X
など)を表します。以前はF=[X]
のようにプレフィックスと角括弧が付いていましたが、変更後は.X
のようにドットとフィールド名が連結されて出力されます。複数のフィールドアクセス(.X.Y.Z
)も正しく表現されます。
- フィールドアクセス(
-
BoolNode
:- 真偽値(
true
,false
)を表します。以前はB=true
のようにプレフィックスが付いていましたが、変更後はtrue
またはfalse
と直接出力されます。
- 真偽値(
-
NumberNode
:- 数値(
1
,-3.2i
など)を表します。以前はN=1
のようにプレフィックスが付いていましたが、変更後は数値そのものが出力されます。
- 数値(
-
StringNode
:- 文字列リテラル(
"hello"
など)を表します。以前はS=%#q
のようにプレフィックスとフォーマット指定子が付いていましたが、変更後は"
hello"
のように、引用符で囲まれた文字列そのものが出力されます。
- 文字列リテラル(
-
BranchNode
(If/Range/With):if
,range
,with
などの制御構造を表します。以前は({{if ...}} ... {{else}} ...)
のように括弧と内部表現が混在していましたが、変更後は{{if .X}}"hello"{{end}}
や{{if .X}}"true"{{else}}"false"{{end}}
のように、実際のテンプレート構文に非常に近い形式で出力されるようになりました。
これらの変更により、parse_test.go
内のテストケースの期待される出力も、内部的なパースツリー表現から、より人間が読みやすいテンプレート構文に更新されています。例えば、以前は [(action: [(command: [F=[X]])])]
となっていたものが、変更後は {{.X}}
となっています。
この変更は、Goのテンプレートパッケージが提供するエラーメッセージの質を大幅に向上させ、ユーザーがテンプレートの構文エラーをより迅速かつ容易に理解し、修正できるようにすることを目的としています。
コアとなるコードの変更箇所
このコミットの主要な変更は src/pkg/text/template/parse/node.go
に集中しています。また、この変更に伴い、テストファイルである src/pkg/html/template/escape_test.go
, src/pkg/text/template/multi_test.go
, src/pkg/text/template/parse/parse_test.go
の期待される出力も更新されています。
以下に、src/pkg/text/template/parse/node.go
の主要な変更箇所を抜粋します。
--- a/src/pkg/text/template/parse/node.go
+++ b/src/pkg/text/template/parse/node.go
@@ -67,11 +67,9 @@ func (l *ListNode) append(n Node) {
func (l *ListNode) String() string {
b := new(bytes.Buffer)
- fmt.Fprint(b, "[")
for _, n := range l.Nodes {
fmt.Fprint(b, n)
}
- fmt.Fprint(b, "]")
return b.String()
}
@@ -86,7 +84,7 @@ func newText(text string) *TextNode {
}
func (t *TextNode) String() string {
- return fmt.Sprintf("(text: %q)", t.Text)
+ return fmt.Sprintf("%q", t.Text)
}
// PipeNode holds a pipeline with optional declaration
@@ -106,10 +104,23 @@ func (p *PipeNode) append(command *CommandNode) {
}
func (p *PipeNode) String() string {
- if p.Decl != nil {
- return fmt.Sprintf("%v := %v", p.Decl, p.Cmds)
+ s := ""
+ if len(p.Decl) > 0 {
+ for i, v := range p.Decl {
+ if i > 0 {
+ s += ", "
+ }
+ s += v.String()
+ }
+ s += " := "
}
- return fmt.Sprintf("%v", p.Cmds)
+ for i, c := range p.Cmds {
+ if i > 0 {
+ s += " | "
+ }
+ s += c.String()
+ }
+ return s
}
// ActionNode holds an action (something bounded by delimiters).
@@ -126,7 +137,8 @@ func newAction(line int, pipe *PipeNode) *ActionNode {
}
func (a *ActionNode) String() string {
- return fmt.Sprintf("(action: %v)", a.Pipe)
+ return fmt.Sprintf("{{%s}}", a.Pipe)
+
}
// CommandNode holds a command (a pipeline inside an evaluating action).
@@ -144,7 +156,14 @@ func (c *CommandNode) append(arg Node) {
}
func (c *CommandNode) String() string {
- return fmt.Sprintf("(command: %v)", c.Args)
+ s := ""
+ for i, arg := range c.Args {
+ if i > 0 {
+ s += " "
+ }
+ s += arg.String()
+ }
+ return s
}
// IdentifierNode holds an identifier.
@@ -159,7 +178,7 @@ func NewIdentifier(ident string) *IdentifierNode {
}
func (i *IdentifierNode) String() string {
- return fmt.Sprintf("I=%s", i.Ident)
+ return i.Ident
}
// VariableNode holds a list of variable names. The dollar sign is
@@ -174,7 +193,14 @@ func newVariable(ident string) *VariableNode {
}
func (v *VariableNode) String() string {
- return fmt.Sprintf("V=%s", v.Ident)
+ s := ""
+ for i, id := range v.Ident {
+ if i > 0 {
+ s += "."
+ }
+ s += id
+ }
+ return s
}
// DotNode holds the special identifier '.'. It is represented by a nil pointer.
@@ -189,7 +215,7 @@ func (d *DotNode) Type() NodeType {
}
func (d *DotNode) String() string {
- return "{{<.>}}"
+ return "."
}
// FieldNode holds a field (identifier starting with '.').
@@ -205,7 +231,11 @@ func newField(ident string) *FieldNode {
}
func (f *FieldNode) String() string {
- return fmt.Sprintf("F=%s", f.Ident)
+ s := ""
+ for _, id := range f.Ident {
+ s += "." + id
+ }
+ return s
}
// BoolNode holds a boolean constant.
@@ -219,7 +249,10 @@ func newBool(true bool) *BoolNode {
}
func (b *BoolNode) String() string {
- return fmt.Sprintf("B=%t", b.True)
+ if b.True {
+ return "true"
+ }
+ return "false"
}
// NumberNode holds a number: signed or unsigned integer, float, or complex.
@@ -337,7 +370,7 @@ func (n *NumberNode) simplifyComplex() {
}
func (n *NumberNode) String() string {
- return fmt.Sprintf("N=%s", n.Text)
+ return n.Text
}
// StringNode holds a string constant. The value has been "unquoted".
@@ -352,7 +385,7 @@ func newString(orig, text string) *StringNode {
}
func (s *StringNode) String() string {
- return fmt.Sprintf("S=%#q", s.Quoted)
+ return s.Quoted
}
// endNode represents an {{end}} action. It is represented by a nil pointer.\
@@ -411,9 +444,9 @@ func (b *BranchNode) String() string {\
panic("unknown branch type")
}\
if b.ElseList != nil {\
- return fmt.Sprintf("({{%s %s}} %s {{else}} %s)", name, b.Pipe, b.List, b.ElseList)
+ return fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}", name, b.Pipe, b.List, b.ElseList)
}\
- return fmt.Sprintf("({{%s %s}} %s)", name, b.Pipe, b.List)
+ return fmt.Sprintf("{{%s %s}}%s{{end}}", name, b.Pipe, b.List)
}
// IfNode represents an {{if}} action and its commands.
コアとなるコードの解説
上記のコード変更は、Goテンプレートのパースツリーを構成する各ノードの String()
メソッドの出力を、デバッグ用の内部表現から、より人間が読みやすいテンプレート構文に変換することを目的としています。
各ノードの変更点の詳細な解説は以下の通りです。
-
ListNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Fprint(b, "[")
とfmt.Fprint(b, "]")
でノードのリスト全体を角括弧で囲んでいました。 - 変更後: これらの角括弧が削除されました。これにより、リスト内の各ノードの文字列表現が単に連結される形になり、より自然な出力となります。例えば、以前は
[(text: " FOO ")]
のようになっていたものが、" FOO "
のように、より簡潔になります。
- 変更前:
-
TextNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("(text: %q)", t.Text)
のように、"(text: ...)"
というプレフィックスと括弧が付いていました。 - 変更後:
fmt.Sprintf("%q", t.Text)
となり、テキストの内容が引用符で囲まれた形式(例:"some text"
)で直接出力されるようになりました。
- 変更前:
-
PipeNode
(src/pkg/text/template/parse/node.go
):- このノードはパイプライン(
|
)や変数宣言(:=
)を表現するため、最も複雑な変更の一つです。 - 変更前:
fmt.Sprintf("%v := %v", p.Decl, p.Cmds)
やfmt.Sprintf("%v", p.Cmds)
のように、内部の宣言やコマンドリストをそのまま出力していました。 - 変更後:
p.Decl
(宣言) が存在する場合、v.String()
を使って各変数をカンマで区切り、最後に:=
を追加します(例:$x, $y :=
)。p.Cmds
(コマンド) の各要素をc.String()
で文字列化し、|
で連結します(例:.X | html | urlquery
)。- これにより、
{{$x := .X | .Y}}
のような、実際のテンプレート構文に近い形式でパイプライン全体が表現されるようになりました。
- このノードはパイプライン(
-
ActionNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("(action: %v)", a.Pipe)
のように、"(action: ...)"
というプレフィックスと括弧が付いていました。 - 変更後:
fmt.Sprintf("{{%s}}", a.Pipe)
となり、内部のPipeNode
の文字列表現を{{
と}}
で囲むことで、{{.X}}
のような実際のテンプレートアクションの形式で出力されるようになりました。
- 変更前:
-
CommandNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("(command: %v)", c.Args)
のように、"(command: ...)"
というプレフィックスと括弧が付いていました。 - 変更後:
arg.String()
を使って各引数をスペースで区切りながら連結するようになりました。これにより、printf %q 23
のような、コマンドとその引数が自然に並んだ形式で出力されます。
- 変更前:
-
IdentifierNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("I=%s", i.Ident)
のように、"I="
というプレフィックスが付いていました。 - 変更後:
return i.Ident
となり、識別子そのもの(例:printf
)が出力されるようになりました。
- 変更前:
-
VariableNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("V=%s", v.Ident)
のように、"V="
というプレフィックスが付いていました。 - 変更後:
id
をドットで連結することで、$x
や$.I
のような変数名がそのまま出力されるようになりました。
- 変更前:
-
DotNode
(src/pkg/text/template/parse/node.go
):- 変更前:
return "{{<.>}}"
という特殊な形式でした。 - 変更後:
return "."
となり、現在のコンテキストを表すドットがそのまま出力されるようになりました。
- 変更前:
-
FieldNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("F=%s", f.Ident)
のように、"F="
というプレフィックスが付いていました。 - 変更後:
"." + id
を連結することで、.X
や.X.Y.Z
のようなフィールドアクセスがそのまま出力されるようになりました。
- 変更前:
-
BoolNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("B=%t", b.True)
のように、"B="
というプレフィックスが付いていました。 - 変更後:
return "true"
またはreturn "false"
となり、真偽値がそのまま出力されるようになりました。
- 変更前:
-
NumberNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("N=%s", n.Text)
のように、"N="
というプレフィックスが付いていました。 - 変更後:
return n.Text
となり、数値がそのまま出力されるようになりました。
- 変更前:
-
StringNode
(src/pkg/text/template/parse/node.go
):- 変更前:
fmt.Sprintf("S=%#q", s.Quoted)
のように、"S="
というプレフィックスと特殊なフォーマット指定子が付いていました。 - 変更後:
return s.Quoted
となり、引用符で囲まれた文字列がそのまま出力されるようになりました。
- 変更前:
-
BranchNode
(src/pkg/text/template/parse/node.go
):if
,range
,with
などの制御構造を表します。- 変更前:
fmt.Sprintf("({{%s %s}} %s {{else}} %s)"
やfmt.Sprintf("({{%s %s}} %s)"
のように、括弧と内部表現が混在していました。 - 変更後:
fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}"
やfmt.Sprintf("{{%s %s}}%s{{end}}"
となり、実際のテンプレート構文({{if ...}}...{{else}}...{{end}}
)に完全に一致する形式で出力されるようになりました。
これらの変更は、Goテンプレートのエラーメッセージが、ユーザーが書いたテンプレートコードと直接対応する形で表示されるようにすることで、デバッグ体験を大幅に改善します。
関連リンク
- Go CL (Change List): https://golang.org/cl/5553066
- GitHub Commit: https://github.com/golang/go/commit/c837e612bd449cd7298ce925749b9f09b54fea48
参考にした情報源リンク
- Go Issue 2644: text/template: improve error messages: https://github.com/golang/go/issues/2644
- Go Documentation - text/template: https://pkg.go.dev/text/template
- Go Documentation - html/template: https://pkg.go.dev/html/template
- Go Documentation - fmt package: https://pkg.go.dev/fmt
- Abstract Syntax Tree (AST) - Wikipedia: https://en.wikipedia.org/wiki/Abstract_syntax_tree
- Go言語のtemplateパッケージのASTを覗いてみる - Qiita: https://qiita.com/tenntenn/items/21122112211221122112 (日本語の参考情報として)