[インデックス 12618] ファイルの概要
このコミットは、Go言語の text/template
パッケージにおける変数の挙動に関する修正です。具体的には、テンプレート内で定義された変数に対して引数を渡すことができないようにする変更が加えられました。これにより、変数を関数のように呼び出す誤用を防ぎ、テンプレートのセマンティクスをより明確にしています。
コミット
commit d6ad6f0e61228152b3618af2e34381439d3b3ca0
Author: Rob Pike <r@golang.org>
Date: Wed Mar 14 10:46:21 2012 +1100
text/template: variables do not take arguments
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5821044
---
src/pkg/text/template/exec.go | 1 +
src/pkg/text/template/exec_test.go | 4 ++++\
2 files changed, 5 insertions(+)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d6ad6f0e61228152b3618af2e34381439d3b3ca0
元コミット内容
text/template: variables do not take arguments
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5821044
変更の背景
Go言語の text/template
パッケージは、テキストベースの出力を生成するためのテンプレートエンジンを提供します。このテンプレート言語では、変数や関数を定義し、それらを使って動的なコンテンツを生成できます。
このコミットが行われる以前は、text/template
のパーサーは、変数に対して引数が与えられた場合でも、構文的にはエラーとせずにパースを完了させていました。しかし、実行時には変数に引数を渡すことは意味をなさず、予期せぬ動作や混乱を招く可能性がありました。例えば、{{$x 2}}
のように変数 $x
に引数 2
を渡そうとする構文は、テンプレート言語の設計上、関数呼び出しにのみ許されるべきものでした。
この変更の背景には、テンプレートのセマンティクスをより厳密にし、ユーザーがテンプレートを記述する際の誤解や誤用を防ぐという目的があります。変数は値を保持するものであり、関数のように引数を受け取って処理を実行するものではないという明確な区別を、実行時にも強制することで、テンプレートの堅牢性と予測可能性を高めています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の text/template
パッケージに関する基本的な知識が必要です。
- Go言語のテンプレートエンジン: Go言語には、
text/template
とhtml/template
の2つの標準テンプレートパッケージがあります。これらは、データ構造をテンプレートに適用してテキスト出力を生成するために使用されます。 - テンプレート構文:
{{...}}
で囲まれた部分がアクションと呼ばれ、変数、関数呼び出し、制御構造(if, rangeなど)を記述します。 - 変数: テンプレート内で
$variableName
の形式で変数を定義し、値を格納できます。例えば、{{$x := 1}}
は変数$x
に値1
を代入します。 - 関数: テンプレート内で
functionName arg1 arg2
の形式で関数を呼び出すことができます。関数は引数を受け取り、結果を返します。 - パイプライン:
|
演算子を使って、前のコマンドの結果を次のコマンドの最後の引数として渡すことができます。例えば、{{.Value | html}}
は.Value
の結果をhtml
関数に渡します。 reflect
パッケージ: Go言語のreflect
パッケージは、実行時に型情報を調べたり、値の操作を行ったりするための機能を提供します。テンプレートエンジンは、このreflect
パッケージを使用して、テンプレートに渡されたデータの型を調べ、適切な処理を行います。parse
パッケージ:text/template
パッケージの内部では、テンプレート文字列を解析して抽象構文木(AST)を構築するためにparse
パッケージが使用されます。parse.VariableNode
は、AST内の変数ノードを表します。
技術的詳細
このコミットの技術的な核心は、text/template
パッケージの実行エンジンが、変数ノードを評価する際に、その変数に引数が渡されていないことを確認するロジックを追加した点にあります。
変更が加えられたのは src/pkg/text/template/exec.go
ファイル内の evalVariableNode
関数です。この関数は、テンプレートの抽象構文木(AST)を走査する際に、変数ノード(parse.VariableNode
)を評価する役割を担っています。
以前のバージョンでは、evalVariableNode
関数は、変数が単一の識別子(例: $x
)である場合に、単にその変数の値を返していました。しかし、この時、もし誤って引数が渡されていたとしても、その引数は無視され、実行時エラーにはなりませんでした。
今回の変更では、if len(v.Ident) == 1
(つまり、変数が単一の識別子である場合)のブロック内に s.notAFunction(args, final)
という新しい呼び出しが追加されました。
s
はstate
構造体のインスタンスであり、テンプレートの実行状態を管理します。notAFunction
メソッドは、渡されたargs
(引数)とfinal
(パイプラインの最終引数かどうかを示すブール値)をチェックします。- もし
args
が空でなく、かつfinal
がfalse
でない(つまり、引数が存在し、かつそれがパイプラインの最終引数ではない)場合、notAFunction
は実行時エラーを発生させます。
これにより、{{$x 2}}
のような構文がパース時にエラーにならなくても、実行時に「変数は引数を取らない」というエラーとして捕捉されるようになりました。
また、src/pkg/text/template/exec_test.go
には、この新しい挙動を検証するためのテストケースが追加されています。
{{3 2}}
: 数値リテラルに引数を渡すケース。数値は関数ではないためエラーとなるべきです。{{$x := 1}}{{$x 2}}
: 変数に引数を渡すケース。変数は関数ではないためエラーとなるべきです。{{$x := 1}}{{3 | $x}}
: パイプラインの最終引数として変数に値を渡すケース。この場合も変数は関数ではないためエラーとなるべきです。
これらのテストケースは、変数が関数のように呼び出された場合に、期待通りに実行時エラーが発生することを確認しています。
コアとなるコードの変更箇所
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -369,6 +369,7 @@ func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
value := s.varValue(v.Ident[0])
if len(v.Ident) == 1 {
+ s.notAFunction(args, final)
return value
}
return s.evalFieldChain(dot, value, v.Ident[1:], args, final)
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -466,6 +466,10 @@ var execTests = []execTest{\n {\"bug6b\", \"{{vfunc .V0 .V0}}\", \"vfunc\", tVal, true},\n {\"bug6c\", \"{{vfunc .V1 .V0}}\", \"vfunc\", tVal, true},\n {\"bug6d\", \"{{vfunc .V1 .V1}}\", \"vfunc\", tVal, true},\n+\t// Legal parse but illegal execution: non-function should have no arguments.\n+\t{\"bug7a\", \"{{3 2}}\", \"\", tVal, false},\n+\t{\"bug7b\", \"{{$x := 1}}{{$x 2}}\", \"\", tVal, false},\n+\t{\"bug7c\", \"{{$x := 1}}{{3 | $x}}\", \"\", tVal, false},\
}\n \n func zeroArgs() string {\
コアとなるコードの解説
src/pkg/text/template/exec.go
の変更
evalVariableNode
関数は、テンプレートの実行中に変数ノードを評価する主要なロジックを含んでいます。
func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args []reflect.Value, final bool) (reflect.Value, error) {
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
value := s.varValue(v.Ident[0]) // 変数名(例: $x)に対応する値を取得
if len(v.Ident) == 1 { // 変数が単一の識別子(例: $x)である場合
s.notAFunction(args, final) // ★追加された行★
return value, nil // 変数の値を返す
}
// 変数がフィールドチェーン(例: $x.Field)である場合、フィールドを評価
return s.evalFieldChain(dot, value, v.Ident[1:], args, final)
}
追加された s.notAFunction(args, final)
の呼び出しがこの変更の肝です。
notAFunction
メソッド(exec.go
内に定義されていると仮定)は、以下のようなロジックを持つと考えられます。
// notAFunction reports an error if args is not empty and final is true.
// It is used to ensure that non-function values are not called with arguments.
func (s *state) notAFunction(args []reflect.Value, final bool) {
if len(args) > 0 && final {
s.errorf("non-function %s called with arguments", s.node.String())
}
}
(※上記の notAFunction
の実装はコミット差分には含まれていませんが、その機能と目的から推測されるものです。実際のGoのソースコードでは、s.errorf
を呼び出してエラーを報告する形になっているはずです。)
このチェックにより、evalVariableNode
が評価しているのが単なる変数であり、かつその変数に引数が渡されている(len(args) > 0
)場合、そしてそれがパイプラインの最終引数として扱われている(final
が true
)場合にエラーが発生します。これにより、{{$x 2}}
のような直接的な変数への引数渡しや、{{3 | $x}}
のようなパイプラインでの変数への引数渡しが実行時に捕捉されるようになります。
src/pkg/text/template/exec_test.go
の変更
追加されたテストケースは、この新しい挙動を具体的に検証します。
var execTests = []execTest{
// ... 既存のテストケース ...
// Legal parse but illegal execution: non-function should have no arguments.
{"bug7a", "{{3 2}}", "", tVal, false},
{"bug7b", "{{$x := 1}}{{$x 2}}", "", tVal, false},
{"bug7c", "{{$x := 1}}{{3 | $x}}", "", tVal, false},
}
各テストケースの最後の false
は、そのテストケースが実行時にエラーを発生させることを期待していることを示しています。
"bug7a", "{{3 2}}", "", tVal, false
: 数値リテラル3
は関数ではないため、引数2
を与えるとエラーになることを期待します。"bug7b", "{{$x := 1}}{{$x 2}}", "", tVal, false
: 変数$x
に値1
が代入されており、これは関数ではないため、引数2
を与えるとエラーになることを期待します。"bug7c", "{{$x := 1}}{{3 | $x}}", "", tVal, false
: パイプラインで3
を変数$x
に渡そうとしています。変数$x
は関数ではないため、エラーになることを期待します。
これらのテストケースが追加されたことで、将来的に同様の回帰バグが発生するのを防ぐことができます。
関連リンク
- Go言語
text/template
パッケージのドキュメント: https://pkg.go.dev/text/template - Go言語
html/template
パッケージのドキュメント: https://pkg.go.dev/html/template - Go言語
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - Go言語
go/parser
パッケージのドキュメント (ASTの理解に役立つ): https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Gerrit Code Review (Goプロジェクトのコードレビューシステム): https://go-review.googlesource.com/
- このコミットのGerritチェンジリスト: https://golang.org/cl/5821044 (コミットメッセージに記載されているリンク)
- Go言語のIssue Tracker: https://github.com/golang/go/issues
- Go言語のブログ (関連する話題が投稿される可能性): https://go.dev/blog/
- Stack Overflow (Go言語のテンプレートに関する一般的な質問と回答): https://stackoverflow.com/questions/tagged/go-templates
- Go言語のメーリングリスト (golang-dev): https://groups.google.com/g/golang-dev (コミットメッセージに記載されているR=golang-dev, CC=golang-devから)