[インデックス 11794] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージのパースツリーノードに、ディープコピー(deep copy)機能を追加するものです。具体的には、Node
インターフェースに Copy()
メソッドが導入され、既存の様々なノード型(ListNode
, TextNode
, PipeNode
など)にその実装が追加されています。これにより、テンプレートの構造を完全に複製することが可能になり、特に html/template
パッケージがテンプレートをコピーする際に役立つとされています。また、この新機能の動作を検証するためのテストも追加されています。
コミット
commit b027a0f11857636314e3e149fc785feb79420e9e
Author: Rob Pike <r@golang.org>
Date: Sat Feb 11 14:21:16 2012 +1100
text/template/parse: deep Copy method for nodes
This will help html/template copy templates.
R=golang-dev, gri, nigeltao, r
CC=golang-dev
https://golang.org/cl/5653062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b027a0f11857636314e3e149fc785feb79420e9e
元コミット内容
text/template/parse: deep Copy method for nodes
This will help html/template copy templates.
R=golang-dev, gri, nigeltao, r
CC=golang-dev
https://golang.org/cl/5653062
変更の背景
Go言語の text/template
および html/template
パッケージは、テキストやHTMLを生成するためのテンプレートエンジンを提供します。これらのパッケージは、テンプレート文字列を解析(パース)して内部的にツリー構造(パースツリー)を構築します。このパースツリーは、テンプレートの実行時にデータと結合されて最終的な出力を生成します。
コミットメッセージにある「This will help html/template copy templates.」という記述から、この変更の主な動機は html/template
パッケージがテンプレートをコピーする際の要件を満たすためであることがわかります。
html/template
パッケージは、セキュリティ上の理由から、HTMLコンテンツを生成する際にクロスサイトスクリプティング(XSS)攻撃を防ぐためのサニタイズ処理を行います。このサニタイズ処理は、パースツリーに対して行われることがあります。テンプレートの実行中に、元のパースツリーを変更することなく、そのコピーに対して操作を行いたい場合や、複数の異なるコンテキストで同じテンプレートの構造を再利用したい場合に、ディープコピー機能が必要となります。
例えば、html/template
がテンプレートを解析した後、そのテンプレートを「安全な」状態にするために、特定のノードを変換したり、追加の情報を付与したりする場合があります。この変換が元のパースツリーに影響を与えないようにするためには、まずパースツリーの完全なコピーを作成し、そのコピーに対して操作を行うのが安全かつ効率的なアプローチとなります。シャローコピー(shallow copy)では、元のツリーとコピーされたツリーが同じ内部データ構造を参照してしまうため、一方の変更が他方に影響を与えてしまう問題が発生します。これを避けるために、参照されているすべての要素も再帰的にコピーするディープコピーが必要とされました。
前提知識の解説
1. Go言語の text/template
および html/template
パッケージ
text/template
: Go言語に組み込まれているテキストテンプレートエンジンです。プレーンテキストの出力生成に使用されます。テンプレートは、Goのデータ構造(構造体、マップ、スライスなど)と結合され、動的なテキストを生成します。html/template
:text/template
と同様の機能を提供しますが、HTML出力に特化しており、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ機能が組み込まれています。これにより、ユーザー入力などの信頼できないデータがHTMLに挿入される際に、自動的に安全な形式に変換されます。
2. パースツリー(Parse Tree / Abstract Syntax Tree - AST)
テンプレートエンジンは、テンプレート文字列を読み込み、その構文を解析して、プログラムが扱いやすい内部的なデータ構造に変換します。このデータ構造がパースツリー、または抽象構文木(AST)です。パースツリーは、テンプレートの各要素(テキスト、変数、条件分岐、ループなど)をノードとして表現し、それらの関係をツリー構造で表します。
例えば、{{if .Condition}}Hello{{end}}
というテンプレートは、以下のようなノードで構成されるツリーとして表現されます。
- ルートノード (List/Root)
- Ifノード
- Pipeノード (条件式
.Condition
) - Listノード (真の場合のブロック)
- Textノード ("Hello")
- ElseListノード (偽の場合のブロック - この例ではnil)
- Pipeノード (条件式
- Ifノード
3. ディープコピー(Deep Copy)とシャローコピー(Shallow Copy)
- シャローコピー(Shallow Copy): オブジェクトをコピーする際に、そのオブジェクトが参照している他のオブジェクトはコピーせず、参照元と同じオブジェクトを参照します。つまり、新しいオブジェクトは作成されますが、その内部のポインタや参照は元のオブジェクトと同じメモリ位置を指します。このため、コピーされたオブジェクトの内部状態を変更すると、元のオブジェクトの内部状態も変更されてしまいます。
- ディープコピー(Deep Copy): オブジェクトをコピーする際に、そのオブジェクトが参照しているすべてのオブジェクトも再帰的にコピーします。これにより、元のオブジェクトとコピーされたオブジェクトは完全に独立した存在となり、一方の変更が他方に影響を与えることはありません。パースツリーのような複雑なネストされたデータ構造を複製する際には、ディープコピーが不可欠です。
4. Go言語のインターフェースと型アサーション
- インターフェース: Go言語におけるインターフェースは、メソッドのシグネチャの集合を定義します。型がそのインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。このコミットでは
Node
インターフェースが定義され、様々なノード型がこれを実装しています。 - 型アサーション: インターフェース型の変数が、特定の具象型であるかどうかをチェックし、その具象型に変換するGoの機能です。例えば、
elem.Copy().(*VariableNode)
は、Copy()
メソッドが返したNode
インターフェースの値を*VariableNode
型に変換しています。これは、特定のノード型に特化した操作を行うために必要です。
技術的詳細
このコミットの主要な変更点は、text/template/parse
パッケージ内のパースツリーノード構造にディープコピー機能を追加したことです。
-
Node
インターフェースへのCopy()
メソッドの追加:Node
インターフェースにCopy() Node
メソッドが追加されました。これにより、すべてのパースツリーノード型がこのメソッドを実装することが義務付けられ、ポリモーフィックなコピーが可能になります。type Node interface { Type() NodeType String() string // Copy does a deep copy of the Node and all its components. // To avoid type assertions, some XxxNodes also have specialized // CopyXxx methods that return *XxxNode. Copy() Node }
コメントにあるように、一部のノード型には、型アサーションを避けるために
CopyXxx
のような特化したコピーメソッドも提供されています。これは、特定のノード型が持つ具体的なフィールドをコピーする際に、より型安全な方法を提供するためです。 -
各ノード型への
Copy()
メソッドの実装:ListNode
,TextNode
,PipeNode
,ActionNode
,CommandNode
,IdentifierNode
,VariableNode
,DotNode
,FieldNode
,BoolNode
,NumberNode
,StringNode
,endNode
,elseNode
,IfNode
,RangeNode
,WithNode
,TemplateNode
など、text/template/parse
パッケージで定義されているほぼすべてのノード型にCopy()
メソッドが実装されました。-
ListNode
のCopyList()
とCopy()
:ListNode
は子ノードのリストを持つため、CopyList()
メソッドが導入され、リスト内の各要素に対して再帰的にelem.Copy()
を呼び出すことでディープコピーを実現しています。func (l *ListNode) CopyList() *ListNode { if l == nil { return l } n := newList() // 新しいListNodeを作成 for _, elem := range l.Nodes { n.append(elem.Copy()) // 各子ノードを再帰的にコピーして追加 } return n } func (l *ListNode) Copy() Node { return l.CopyList() }
-
TextNode
のCopy()
:TextNode
はバイトスライスText
を持つため、append([]byte{}, t.Text...)
を使用して新しいスライスを作成し、元のスライスの内容をコピーすることでディープコピーを実現しています。これにより、元のText
スライスとコピーされたText
スライスが異なるメモリ領域を指すようになります。func (t *TextNode) Copy() Node { return &TextNode{NodeType: NodeText, Text: append([]byte{}, t.Text...)} }
-
PipeNode
のCopyPipe()
とCopy()
:PipeNode
はDecl
(変数宣言) とCmds
(コマンド) のスライスを持つため、これらも再帰的にコピーされます。特にDecl
の要素は*VariableNode
に、Cmds
の要素は*CommandNode
に型アサーションしてコピーしています。func (p *PipeNode) CopyPipe() *PipeNode { if p == nil { return p } var decl []*VariableNode for _, d := range p.Decl { decl = append(decl, d.Copy().(*VariableNode)) // VariableNodeをコピー } n := newPipeline(p.Line, decl) for _, c := range p.Cmds { n.append(c.Copy().(*CommandNode)) // CommandNodeをコピー } return n } func (p *PipeNode) Copy() Node { return p.CopyPipe() }
-
BranchNode
を埋め込むノード(IfNode
,RangeNode
,WithNode
)のCopy()
: これらのノードはBranchNode
を埋め込んでおり、Pipe
,List
,ElseList
といった子ノードを持つため、それぞれに対してCopyPipe()
やCopyList()
を再帰的に呼び出してディープコピーを実現しています。func (i *IfNode) Copy() Node { return newIf(i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList()) }
-
NumberNode
のCopy()
:NumberNode
は値がプリミティブ型に近い構造を持つため、構造体全体を値渡しでコピーし、そのポインタを返すことで効率的にディープコピーを実現しています。func (n *NumberNode) Copy() Node { nn := new(NumberNode) *nn = *n // Easy, fast, correct. return nn }
-
-
テストの追加と修正 (
parse_test.go
):- 既存の
TestParse
関数がtestParse(doCopy bool, t *testing.T)
というヘルパー関数にリファクタリングされました。 testParse
関数はdoCopy
引数を受け取り、これがtrue
の場合はパースツリーのルートノードに対してCopy()
メソッドを呼び出し、その結果のString()
表現をテストします。- 新しく
TestParseCopy
関数が追加され、testParse(true, t)
を呼び出すことで、ディープコピー機能が正しく動作するかどうかを検証しています。これにより、コピーされたツリーが元のツリーと同じ文字列表現を生成することを確認しています。
- 既存の
これらの変更により、text/template
のパースツリーは完全に独立した形で複製できるようになり、html/template
のような上位レイヤーのパッケージが、元のテンプレート構造を破壊することなく、安全に操作を行える基盤が提供されました。
コアとなるコードの変更箇所
src/pkg/text/template/parse/node.go
Node
インターフェースにCopy() Node
メソッドを追加。ListNode
にCopyList() *ListNode
とCopy() Node
メソッドを追加。TextNode
にCopy() Node
メソッドを追加。PipeNode
にCopyPipe() *PipeNode
とCopy() Node
メソッドを追加。ActionNode
にCopy() Node
メソッドを追加。CommandNode
にCopy() Node
メソッドを追加。IdentifierNode
にCopy() Node
メソッドを追加。VariableNode
にCopy() Node
メソッドを追加。DotNode
にCopy() Node
メソッドを追加。FieldNode
にCopy() Node
メソッドを追加。BoolNode
にCopy() Node
メソッドを追加。NumberNode
にCopy() Node
メソッドを追加。StringNode
にCopy() Node
メソッドを追加。endNode
にCopy() Node
メソッドを追加。elseNode
にCopy() Node
メソッドを追加。IfNode
にCopy() Node
メソッドを追加。RangeNode
にCopy() Node
メソッドを追加。WithNode
にCopy() Node
メソッドを追加。TemplateNode
にCopy() Node
メソッドを追加。
src/pkg/text/template/parse/parse_test.go
TestParse
関数をtestParse(doCopy bool, t *testing.T)
にリファクタリング。testParse
内でdoCopy
フラグに応じてtmpl.Root.Copy().String()
を呼び出すロジックを追加。- 新しいテスト関数
TestParseCopy(t *testing.T)
を追加し、testParse(true, t)
を呼び出すことでコピー機能のテストを実行。
コアとなるコードの解説
src/pkg/text/template/parse/node.go
の変更
このファイルでは、テンプレートのパースツリーを構成する様々なノード型が定義されています。今回の変更の核心は、これらのノード型が Node
インターフェースに新しく追加された Copy()
メソッドを実装することです。
-
Node
インターフェース:type Node interface { Type() NodeType String() string Copy() Node // 新しく追加されたメソッド }
この変更により、すべてのノードは自身のディープコピーを返す責任を持つことになります。
-
ListNode
のCopyList()
とCopy()
:ListNode
は複数の子ノードを持つため、そのNodes
スライス内の各elem
に対してelem.Copy()
を再帰的に呼び出し、新しいListNode
に追加しています。これにより、リストとその中のすべてのノードが完全に複製されます。func (l *ListNode) CopyList() *ListNode { if l == nil { return l } // nilチェック n := newList() // 新しいリストノードのインスタンスを作成 for _, elem := range l.Nodes { n.append(elem.Copy()) // 各子ノードのコピーを新しいリストに追加 } return n } func (l *ListNode) Copy() Node { return l.CopyList() // インターフェースの要件を満たすためにCopyListを呼び出す }
-
TextNode
のCopy()
:TextNode
はText
というバイトスライスを持っています。スライスは参照型なので、シャローコピーでは元のスライスと同じメモリを参照してしまいます。append([]byte{}, t.Text...)
は、新しいバイトスライスを作成し、元のt.Text
の内容をその新しいスライスにコピーするイディオムです。これにより、Text
の内容が完全に独立して複製されます。func (t *TextNode) Copy() Node { return &TextNode{NodeType: NodeText, Text: append([]byte{}, t.Text...)} }
-
PipeNode
のCopyPipe()
とCopy()
:PipeNode
はDecl
(変数宣言) とCmds
(コマンド) という2つのスライスを持ち、それぞれがさらにノードを含んでいます。これらのスライス内の各要素(*VariableNode
や*CommandNode
)に対してもCopy()
を呼び出し、型アサーション.(*VariableNode)
や.(*CommandNode)
を用いて適切な型に変換して新しいスライスに格納しています。これにより、パイプラインの構造全体がディープコピーされます。func (p *PipeNode) CopyPipe() *PipeNode { if p == nil { return p } var decl []*VariableNode // 新しい変数宣言スライス for _, d := range p.Decl { decl = append(decl, d.Copy().(*VariableNode)) // 各VariableNodeをコピー } n := newPipeline(p.Line, decl) // 新しいパイプラインノードを作成 for _, c := range p.Cmds { n.append(c.Copy().(*CommandNode)) // 各CommandNodeをコピー } return n } func (p *PipeNode) Copy() Node { return p.CopyPipe() }
-
NumberNode
のCopy()
:NumberNode
は数値の値を保持しており、その内部構造は比較的単純です。*nn = *n
という行は、n
が指すNumberNode
の内容を、新しく作成されたnn
が指すNumberNode
に値渡しでコピーしています。これにより、NumberNode
のすべてのフィールドが効率的に複製されます。func (n *NumberNode) Copy() Node { nn := new(NumberNode) *nn = *n // 構造体の内容を値渡しでコピー return nn }
src/pkg/text/template/parse/parse_test.go
の変更
このファイルでは、テンプレートのパース処理が正しく行われることを検証するテストが定義されています。
-
testParse
ヘルパー関数の導入: 既存のTestParse
のロジックがtestParse
という新しい関数に抽出されました。この関数はdoCopy
というブール引数を受け取ります。func testParse(doCopy bool, t *testing.T) { // ... var result string if doCopy { result = tmpl.Root.Copy().String() // doCopyがtrueの場合、コピーしたノードのString()をテスト } else { result = tmpl.Root.String() // 通常のテストでは元のノードのString()をテスト } // ... }
この変更により、同じテストケースセットを使用して、元のパースツリーの文字列表現と、ディープコピーされたパースツリーの文字列表現の両方を検証できるようになりました。
-
TestParseCopy
の追加: 新しく追加されたTestParseCopy
関数は、testParse(true, t)
を呼び出すことで、ディープコピー機能が正しく動作するかどうかを明示的にテストします。コピーされたパースツリーが、元のパースツリーと同じ構造と内容を持つことを、その文字列表現を比較することで確認しています。func TestParse(t *testing.T) { testParse(false, t) // 既存のテストはdoCopy=falseで実行 } // Same as TestParse, but we copy the node first func TestParseCopy(t *testing.T) { testParse(true, t) // 新しいテストはdoCopy=trueで実行 }
これらのコード変更により、text/template
のパースツリーは、その複雑なネストされた構造を含めて完全に独立した形で複製できるようになり、html/template
のようなパッケージが、元のテンプレート構造を破壊することなく、安全に操作を行える強固な基盤が提供されました。
関連リンク
- Go言語
text/template
パッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語
html/template
パッケージ公式ドキュメント: https://pkg.go.dev/html/template - Go言語の型アサーションに関する公式ドキュメント: https://go.dev/tour/methods/15
参考にした情報源リンク
- Go言語のソースコード (text/template/parse): https://github.com/golang/go/tree/master/src/text/template/parse
- Go言語のコミット履歴: https://github.com/golang/go/commits/master
- Gerrit Change-ID 5653062: https://golang.org/cl/5653062 (これはGitHubのコミットページにリダイレクトされますが、元のGerritのIDです)
- ディープコピーとシャローコピーに関する一般的なプログラミング概念の解説 (例: Wikipedia, プログラミング関連ブログなど)