Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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)

3. ディープコピー(Deep Copy)とシャローコピー(Shallow Copy)

  • シャローコピー(Shallow Copy): オブジェクトをコピーする際に、そのオブジェクトが参照している他のオブジェクトはコピーせず、参照元と同じオブジェクトを参照します。つまり、新しいオブジェクトは作成されますが、その内部のポインタや参照は元のオブジェクトと同じメモリ位置を指します。このため、コピーされたオブジェクトの内部状態を変更すると、元のオブジェクトの内部状態も変更されてしまいます。
  • ディープコピー(Deep Copy): オブジェクトをコピーする際に、そのオブジェクトが参照しているすべてのオブジェクトも再帰的にコピーします。これにより、元のオブジェクトとコピーされたオブジェクトは完全に独立した存在となり、一方の変更が他方に影響を与えることはありません。パースツリーのような複雑なネストされたデータ構造を複製する際には、ディープコピーが不可欠です。

4. Go言語のインターフェースと型アサーション

  • インターフェース: Go言語におけるインターフェースは、メソッドのシグネチャの集合を定義します。型がそのインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。このコミットでは Node インターフェースが定義され、様々なノード型がこれを実装しています。
  • 型アサーション: インターフェース型の変数が、特定の具象型であるかどうかをチェックし、その具象型に変換するGoの機能です。例えば、elem.Copy().(*VariableNode) は、Copy() メソッドが返した Node インターフェースの値を *VariableNode 型に変換しています。これは、特定のノード型に特化した操作を行うために必要です。

技術的詳細

このコミットの主要な変更点は、text/template/parse パッケージ内のパースツリーノード構造にディープコピー機能を追加したことです。

  1. 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 のような特化したコピーメソッドも提供されています。これは、特定のノード型が持つ具体的なフィールドをコピーする際に、より型安全な方法を提供するためです。

  2. 各ノード型への Copy() メソッドの実装: ListNode, TextNode, PipeNode, ActionNode, CommandNode, IdentifierNode, VariableNode, DotNode, FieldNode, BoolNode, NumberNode, StringNode, endNode, elseNode, IfNode, RangeNode, WithNode, TemplateNode など、text/template/parse パッケージで定義されているほぼすべてのノード型に Copy() メソッドが実装されました。

    • ListNodeCopyList()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()
      }
      
    • TextNodeCopy(): TextNode はバイトスライス Text を持つため、append([]byte{}, t.Text...) を使用して新しいスライスを作成し、元のスライスの内容をコピーすることでディープコピーを実現しています。これにより、元の Text スライスとコピーされた Text スライスが異なるメモリ領域を指すようになります。

      func (t *TextNode) Copy() Node {
          return &TextNode{NodeType: NodeText, Text: append([]byte{}, t.Text...)}
      }
      
    • PipeNodeCopyPipe()Copy(): PipeNodeDecl (変数宣言) と 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())
      }
      
    • NumberNodeCopy(): NumberNode は値がプリミティブ型に近い構造を持つため、構造体全体を値渡しでコピーし、そのポインタを返すことで効率的にディープコピーを実現しています。

      func (n *NumberNode) Copy() Node {
          nn := new(NumberNode)
          *nn = *n // Easy, fast, correct.
          return nn
      }
      
  3. テストの追加と修正 (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 メソッドを追加。
  • ListNodeCopyList() *ListNodeCopy() Node メソッドを追加。
  • TextNodeCopy() Node メソッドを追加。
  • PipeNodeCopyPipe() *PipeNodeCopy() Node メソッドを追加。
  • ActionNodeCopy() Node メソッドを追加。
  • CommandNodeCopy() Node メソッドを追加。
  • IdentifierNodeCopy() Node メソッドを追加。
  • VariableNodeCopy() Node メソッドを追加。
  • DotNodeCopy() Node メソッドを追加。
  • FieldNodeCopy() Node メソッドを追加。
  • BoolNodeCopy() Node メソッドを追加。
  • NumberNodeCopy() Node メソッドを追加。
  • StringNodeCopy() Node メソッドを追加。
  • endNodeCopy() Node メソッドを追加。
  • elseNodeCopy() Node メソッドを追加。
  • IfNodeCopy() Node メソッドを追加。
  • RangeNodeCopy() Node メソッドを追加。
  • WithNodeCopy() Node メソッドを追加。
  • TemplateNodeCopy() 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 // 新しく追加されたメソッド
    }
    

    この変更により、すべてのノードは自身のディープコピーを返す責任を持つことになります。

  • ListNodeCopyList()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を呼び出す
    }
    
  • TextNodeCopy(): TextNodeText というバイトスライスを持っています。スライスは参照型なので、シャローコピーでは元のスライスと同じメモリを参照してしまいます。append([]byte{}, t.Text...) は、新しいバイトスライスを作成し、元の t.Text の内容をその新しいスライスにコピーするイディオムです。これにより、Text の内容が完全に独立して複製されます。

    func (t *TextNode) Copy() Node {
        return &TextNode{NodeType: NodeText, Text: append([]byte{}, t.Text...)}
    }
    
  • PipeNodeCopyPipe()Copy(): PipeNodeDecl (変数宣言) と 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()
    }
    
  • NumberNodeCopy(): 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 のようなパッケージが、元のテンプレート構造を破壊することなく、安全に操作を行える強固な基盤が提供されました。

関連リンク

参考にした情報源リンク