[インデックス 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, プログラミング関連ブログなど)