[インデックス 13712] ファイルの概要
このコミットは、Go言語の exp/html
パッケージにおけるHTMLノードの内部表現を、スライス([]*Node
)から双方向連結リストに変更し、それに伴いAPIの名称をDOM(Document Object Model)の慣習に合わせる変更です。この変更により、HTMLパーサーのパフォーマンスが向上し、メモリ割り当てが削減されました。
コミット
commit 13cf2473b8b617a3e800a22b810a5a13030d4cb6
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Aug 31 10:00:12 2012 +1000
exp/html: change a node's children from a slice to a linked list.
Also rename Node.{Add,Remove} to Node.{AppendChild,RemoveChild} to
be consistent with the DOM.
benchmark old ns/op new ns/op delta
BenchmarkParser 4042040 3749618 -7.23%
benchmark old MB/s new MB/s speedup
BenchmarkParser 19.34 20.85 1.08x
BenchmarkParser mallocs per iteration is also:
10495 before / 7992 after
R=andybalholm, r, adg
CC=golang-dev
https://golang.org/cl/6495061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13cf2473b8b617a3e800a22b810a5a13030d4cb6
元コミット内容
exp/html
パッケージにおいて、ノードの子要素の管理方法をスライスから連結リストに変更しました。また、Node.Add
および Node.Remove
メソッドを Node.AppendChild
および Node.RemoveChild
にリネームし、DOMの命名規則に準拠させました。この変更により、BenchmarkParser
の実行時間が7.23%短縮され、スループットが1.08倍に向上し、1イテレーションあたりのメモリ割り当て回数が10495回から7992回に削減されました。
変更の背景
この変更の主な背景には、以下の点が挙げられます。
- パフォーマンスの最適化: HTMLパーシングは、特に大規模なドキュメントを扱う際にCPUとメモリを大量に消費する処理です。既存のスライスベースの実装では、子ノードの追加や削除の際に、スライスの再割り当てや要素の移動が発生し、これが性能ボトルネックとなっていました。連結リストへの変更は、これらの操作の計算量を削減し、全体的なパーシング速度とメモリ効率を向上させることを目的としています。コミットメッセージに示されているベンチマーク結果は、この目的が達成されたことを明確に示しています。
- メモリ使用量の削減: スライスは連続したメモリ領域を必要とし、容量が不足するとより大きな領域にコピーされるため、不要なメモリ割り当てが発生する可能性があります。連結リストは各ノードが独立してメモリを確保し、ポインタで接続されるため、より効率的なメモリ利用が期待できます。特に、ノードの追加・削除が頻繁に行われるパーシング処理において、メモリ割り当て回数の削減はガベージコレクションの負荷軽減にも繋がり、全体的なパフォーマンス向上に寄与します。
- DOMとの一貫性: Web標準であるDocument Object Model (DOM) は、HTMLやXMLドキュメントの構造を表現し、プログラムからアクセス・操作するためのAPIを定義しています。DOMでは、子ノードの追加や削除に
appendChild
やremoveChild
といったメソッドが一般的に使用されます。exp/html
パッケージのAPIをDOMの命名規則に合わせることで、Web開発者がより直感的にAPIを使用できるようになり、他のDOM実装との相互運用性や学習コストの削減に貢献します。
前提知識の解説
Go言語の exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリの一部として提供されているHTMLパーサーおよびレンダラーです。このパッケージは、HTML5の仕様に厳密に従ってHTMLドキュメントを解析し、DOMツリーのような構造(Node
型)に変換する機能を提供します。Webスクレイピング、HTMLテンプレートエンジンの実装、HTMLのサニタイズなど、様々な用途で利用されます。exp
というプレフィックスは、実験的なパッケージであることを示しており、将来的にGoの標準ライブラリに統合される可能性を秘めていました(後に golang.org/x/net/html
として提供されています)。
データ構造: スライスと連結リスト
- スライス (Slice): Go言語におけるスライスは、配列を抽象化したもので、可変長シーケンスを表現します。内部的には、基となる配列へのポインタ、長さ(
len
)、容量(cap
)を持ちます。要素へのアクセスはインデックスによってO(1)で高速に行えますが、要素の追加や削除(特に中間への挿入・削除)は、その後の要素を移動させる必要があるため、最悪の場合O(N)の計算量がかかります。また、容量を超えた追加が行われると、より大きな新しい配列が割り当てられ、既存の要素がコピーされるため、メモリ割り当てとコピーのオーバーヘッドが発生します。 - 連結リスト (Linked List): 連結リストは、ノードがデータと次のノードへのポインタ(単方向連結リストの場合)または前後のノードへのポインタ(双方向連結リストの場合)を持つデータ構造です。要素の追加や削除は、ポインタの付け替えだけで済むため、O(1)の計算量で行えます。しかし、特定の位置の要素にアクセスするには、リストの先頭から順に辿る必要があるため、最悪の場合O(N)の計算量がかかります。メモリは連続している必要がなく、各ノードが個別に割り当てられます。
DOM (Document Object Model)
DOMは、HTMLやXMLドキュメントの論理構造をツリー形式で表現するためのプログラミングインターフェースです。WebブラウザはHTMLドキュメントを解析してDOMツリーを構築し、JavaScriptなどのスクリプト言語からこのツリーを操作することで、Webページのコンテンツ、構造、スタイルを動的に変更できます。DOMはW3Cによって標準化されており、appendChild
, removeChild
, insertBefore
などのメソッドは、DOMツリーのノードを操作するための基本的なAPIとして広く認識されています。
Foster Parenting (フォスターペアレンティング)
HTML5のパーシングアルゴリズムにおける「フォスターペアレンティング」は、特定の状況下でHTML要素が通常の親子関係のルールに従わず、別の親ノードに「里子」として追加されるメカニズムです。これは主に、<table>
要素の内部で不正なマークアップ(例: <table><div>...</div></table>
)が検出された場合に発生します。ブラウザはエラーを修正しようとし、div
のようなブロック要素を table
の外に移動させたり、table
の直前の兄弟要素として配置したりします。このメカケニズムは、HTMLの堅牢なエラー回復処理の一部であり、不正なHTMLでも可能な限りレンダリングを試みるために重要です。
技術的詳細
このコミットの核となる技術的変更は、Node
構造体の Child
フィールドが []*Node
(スライス) から、FirstChild
, LastChild
, PrevSibling
, NextSibling
といったポインタを持つ双方向連結リスト形式に変更された点です。
変更前 (Node
構造体の一部):
type Node struct {
Parent *Node
Child []*Node // 子ノードをスライスで保持
// ...
}
// Add adds a node as a child of n.
func (n *Node) Add(child *Node) {
if child.Parent != nil {
panic("html: Node.Add called for a child Node that already has a parent")
}
child.Parent = n
n.Child = append(n.Child, child) // スライスへの追加
}
// Remove removes a node as a child of n.
func (n *Node) Remove(child *Node) {
if child.Parent == n {
child.Parent = nil
for i, m := range n.Child {
if m == child {
copy(n.Child[i:], n.Child[i+1:]) // スライス要素の移動
j := len(n.Child) - 1
n.Child[j] = nil
n.Child = n.Child[:j]
return
}
}
}
panic("html: Node.Remove called for a non-child Node")
}
変更後 (Node
構造体の一部):
type Node struct {
Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node // 連結リストのポインタ
// ...
}
// InsertBefore inserts newChild as a child of n, immediately before oldChild.
func (n *Node) InsertBefore(newChild, oldChild *Node) {
// ... ポインタ操作による挿入ロジック ...
}
// AppendChild adds a node c as a child of n.
func (n *Node) AppendChild(c *Node) {
// ... ポインタ操作による追加ロジック ...
}
// RemoveChild removes a node c that is a child of n.
func (n *Node) RemoveChild(c *Node) {
// ... ポインタ操作による削除ロジック ...
}
この変更により、Node.Add
と Node.Remove
はそれぞれ Node.AppendChild
と Node.RemoveChild
にリネームされ、内部実装がスライス操作からポインタ操作に変わりました。
パフォーマンスへの影響:
- CPU使用率の削減: スライスでの要素の追加・削除は、内部配列の再割り当てや要素のコピーを伴うため、CPUサイクルを消費します。連結リストではポインタの付け替えのみで済むため、これらのオーバーヘッドがなくなります。特にHTMLパーシングのように、ツリー構造が頻繁に構築・変更される処理では、この差が顕著に現れます。
- メモリ割り当ての削減: スライスが容量不足になった際に発生する新しい配列の割り当てとコピーが不要になります。コミットメッセージにある
mallocs per iteration
の削減は、この効果を直接的に示しています。メモリ割り当てが減ることで、ガベージコレクションの頻度も減少し、アプリケーション全体の応答性が向上します。 - キャッシュ効率: 一般的に、スライスのような連続したメモリ領域はCPUキャッシュに乗りやすく、アクセスが高速になる傾向があります。一方、連結リストはメモリが分散しているため、キャッシュミスが発生しやすくなる可能性があります。しかし、HTMLノードのようなツリー構造では、ノード間の参照が頻繁に発生するため、必ずしもスライスが有利とは限りません。このコミットのベンチマーク結果は、連結リストへの変更が全体としてパフォーマンス向上に寄与したことを示しており、メモリ割り当ての削減とCPUオーバーヘッドの低減がキャッシュ効率の潜在的なデメリットを上回ったと考えられます。
APIの変更:
Node.Add(child *Node)
->Node.AppendChild(c *Node)
Node.Remove(child *Node)
->Node.RemoveChild(c *Node)
- 新たに
Node.InsertBefore(newChild, oldChild *Node)
が追加されました。これはDOMのinsertBefore
メソッドに相当し、既存の子ノードの前に新しい子ノードを挿入する機能を提供します。
これらのAPI変更は、DOMの標準的な命名規則と操作パターンに合わせることで、開発者にとってより自然で予測可能なインターフェースを提供します。
コアとなるコードの変更箇所
主な変更は以下のファイルに集中しています。
src/pkg/exp/html/node.go
:Node
構造体の定義と、子ノード操作メソッド(Add
,Remove
の削除とInsertBefore
,AppendChild
,RemoveChild
の追加)の実装。reparentChildren
関数も連結リスト操作に更新されています。src/pkg/exp/html/node_test.go
: 新たに作成されたテストファイルで、ノードの親子・兄弟関係の整合性をチェックするcheckTreeConsistency
およびcheckNodeConsistency
関数が追加されています。これは連結リストのポインタ操作が正しく行われていることを検証するために非常に重要です。src/pkg/exp/html/parse.go
: HTMLパーサーのロジックが、変更されたNode
の子ノード操作API(Add
からAppendChild
など)を使用するように更新されています。特にaddChild
,fosterParent
,addText
などの関数で変更が見られます。src/pkg/exp/html/parse_test.go
: パーサーのテストコードが、新しいAPIと整合性チェックを使用するように更新されています。src/pkg/exp/html/render.go
: HTMLレンダラーが、ノードの子要素をイテレートする際にスライスではなく連結リストのポインタ(FirstChild
,NextSibling
)を使用するように変更されています。src/pkg/exp/html/render_test.go
: レンダラーのテストコードが、新しいノード構造に合わせて更新されています。
コアとなるコードの解説
src/pkg/exp/html/node.go
-
Node
構造体の変更:type Node struct { Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node Type NodeType DataAtom atom.Atom Data string Attr []Attribute }
Child []*Node
が削除され、FirstChild
,LastChild
,PrevSibling
,NextSibling
といったポインタが追加されました。これにより、各ノードは自身の子の最初と最後のノード、そして自身の前後の兄弟ノードへの参照を持つことになります。 -
InsertBefore
メソッド: 新しい子ノードnewChild
を、既存の子ノードoldChild
の直前に挿入します。oldChild
がnil
の場合は、newChild
を末尾に追加します。このメソッドは、連結リストのポインタを適切に更新することで、O(1)で挿入操作を実現します。 -
AppendChild
メソッド: 新しい子ノードc
を、現在のノードn
の子リストの末尾に追加します。InsertBefore
と同様に、ポインタ操作のみで行われます。 -
RemoveChild
メソッド: 子ノードc
を現在のノードn
の子リストから削除します。削除後、c
は親も兄弟も持たない状態になります。これもポインタ操作のみでO(1)で実行されます。 -
reparentChildren
関数:src
ノードの全ての子ノードをdst
ノードの子として再親付けする関数です。変更前はスライス操作(append
とcopy
)で行われていましたが、変更後はsrc.RemoveChild(child)
とdst.AppendChild(child)
をループ内で呼び出すことで、連結リストの操作に置き換えられています。これにより、効率的なノードの移動が可能になります。
src/pkg/exp/html/node_test.go
checkTreeConsistency
およびcheckNodeConsistency
: これらの関数は、ノードツリー全体の整合性、特に親子・兄弟関係のポインタが正しく設定されているかを検証するためのものです。無限ループの検出、親子の不一致、兄弟関係の不整合、重複する子ノードなどをチェックします。このような低レベルのデータ構造変更においては、テストによる厳密な整合性チェックが不可欠です。
src/pkg/exp/html/parse.go
および src/pkg/exp/html/render.go
これらのファイルでは、HTMLの解析とレンダリングのロジックが、Node
構造体の変更と新しいAPIに合わせて更新されています。例えば、子ノードの追加は p.top().Add(n)
から p.top().AppendChild(n)
に変更され、子ノードのイテレーションは for _, c := range n.Child
から for c := n.FirstChild; c != nil; c = c.NextSibling
に変更されています。これにより、パーサーとレンダラーは新しい連結リストベースのノード構造を正しく利用できるようになります。
関連リンク
- Go言語の
exp/html
パッケージ (現在のgolang.org/x/net/html
): https://pkg.go.dev/golang.org/x/net/html - Document Object Model (DOM) の概要: https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model
- HTML5 パーシングアルゴリズム (W3C勧告): https://html.spec.whatwg.org/multipage/parsing.html (特に "foster parenting" のセクション)
参考にした情報源リンク
- Go Slices: usage and internals: https://go.dev/blog/slices
- Linked List (Wikipedia): https://ja.wikipedia.org/wiki/%E9%80%A3%E7%B5%90%E3%83%AA%E3%82%B9%E3%83%88
- HTML Standard - 13.2.6.2 The "in body" insertion mode (foster parenting): https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting
- Go CL 6495061 (Gerrit Code Review): https://go-review.googlesource.com/c/6495061 (これはコミットメッセージに記載されているCLへのリンクであり、詳細な変更内容やレビューコメントが含まれています。)
- GoDoc for
exp/html
(当時のドキュメント): https://godoc.org/exp/html (現在はgolang.org/x/net/html
にリダイレクトされる可能性があります) - Go言語のメモリ管理とガベージコレクションに関する一般的な情報源 (例: Goの公式ドキュメントやブログ記事)
- データ構造とアルゴリズムに関する一般的な書籍やオンラインリソース (スライスと連結リストの比較など)
- Webブラウザのレンダリングエンジンに関する情報 (DOMツリーの構築プロセスなど)
- HTML5仕様書 (W3CまたはWHATWG)
- Go言語のベンチマークに関するドキュメント# [インデックス 13712] ファイルの概要
このコミットは、Go言語の exp/html
パッケージにおけるHTMLノードの内部表現を、スライス([]*Node
)から双方向連結リストに変更し、それに伴いAPIの名称をDOM(Document Object Model)の慣習に合わせる変更です。この変更により、HTMLパーサーのパフォーマンスが向上し、メモリ割り当てが削減されました。
コミット
commit 13cf2473b8b617a3e800a22b810a5a13030d4cb6
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Aug 31 10:00:12 2012 +1000
exp/html: change a node's children from a slice to a linked list.
Also rename Node.{Add,Remove} to Node.{AppendChild,RemoveChild} to
be consistent with the DOM.
benchmark old ns/op new ns/op delta
BenchmarkParser 4042040 3749618 -7.23%
benchmark old MB/s new MB/s speedup
BenchmarkParser 19.34 20.85 1.08x
BenchmarkParser mallocs per iteration is also:
10495 before / 7992 after
R=andybalholm, r, adg
CC=golang-dev
https://golang.org/cl/6495061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13cf2473b8b617a3e800a22b810a5a13030d4cb6
元コミット内容
exp/html
パッケージにおいて、ノードの子要素の管理方法をスライスから連結リストに変更しました。また、Node.Add
および Node.Remove
メソッドを Node.AppendChild
および Node.RemoveChild
にリネームし、DOMの命名規則に準拠させました。この変更により、BenchmarkParser
の実行時間が7.23%短縮され、スループットが1.08倍に向上し、1イテレーションあたりのメモリ割り当て回数が10495回から7992回に削減されました。
変更の背景
この変更の主な背景には、以下の点が挙げられます。
- パフォーマンスの最適化: HTMLパーシングは、特に大規模なドキュメントを扱う際にCPUとメモリを大量に消費する処理です。既存のスライスベースの実装では、子ノードの追加や削除の際に、スライスの再割り当てや要素の移動が発生し、これが性能ボトルネックとなっていました。連結リストへの変更は、これらの操作の計算量を削減し、全体的なパーシング速度とメモリ効率を向上させることを目的としています。コミットメッセージに示されているベンチマーク結果は、この目的が達成されたことを明確に示しています。
- メモリ使用量の削減: スライスは連続したメモリ領域を必要とし、容量が不足するとより大きな領域にコピーされるため、不要なメモリ割り当てが発生する可能性があります。連結リストは各ノードが独立してメモリを確保し、ポインタで接続されるため、より効率的なメモリ利用が期待できます。特に、ノードの追加・削除が頻繁に行われるパーシング処理において、メモリ割り当て回数の削減はガベージコレクションの負荷軽減にも繋がり、全体的なパフォーマンス向上に寄与します。
- DOMとの一貫性: Web標準であるDocument Object Model (DOM) は、HTMLやXMLドキュメントの構造を表現し、プログラムからアクセス・操作するためのAPIを定義しています。DOMでは、子ノードの追加や削除に
appendChild
やremoveChild
といったメソッドが一般的に使用されます。exp/html
パッケージのAPIをDOMの命名規則に合わせることで、Web開発者がより直感的にAPIを使用できるようになり、他のDOM実装との相互運用性や学習コストの削減に貢献します。
前提知識の解説
Go言語の exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリの一部として提供されているHTMLパーサーおよびレンダラーです。このパッケージは、HTML5の仕様に厳密に従ってHTMLドキュメントを解析し、DOMツリーのような構造(Node
型)に変換する機能を提供します。Webスクレイピング、HTMLテンプレートエンジンの実装、HTMLのサニタイズなど、様々な用途で利用されます。exp
というプレフィックスは、実験的なパッケージであることを示しており、将来的にGoの標準ライブラリに統合される可能性を秘めていました(後に golang.org/x/net/html
として提供されています)。
データ構造: スライスと連結リスト
- スライス (Slice): Go言語におけるスライスは、配列を抽象化したもので、可変長シーケンスを表現します。内部的には、基となる配列へのポインタ、長さ(
len
)、容量(cap
)を持ちます。要素へのアクセスはインデックスによってO(1)で高速に行えますが、要素の追加や削除(特に中間への挿入・削除)は、その後の要素を移動させる必要があるため、最悪の場合O(N)の計算量がかかります。また、容量を超えた追加が行われると、より大きな新しい配列が割り当てられ、既存の要素がコピーされるため、メモリ割り当てとコピーのオーバーヘッドが発生します。 - 連結リスト (Linked List): 連結リストは、ノードがデータと次のノードへのポインタ(単方向連結リストの場合)または前後のノードへのポインタ(双方向連結リストの場合)を持つデータ構造です。要素の追加や削除は、ポインタの付け替えだけで済むため、O(1)の計算量で行えます。しかし、特定の位置の要素にアクセスするには、リストの先頭から順に辿る必要があるため、最悪の場合O(N)の計算量がかかります。メモリは連続している必要がなく、各ノードが個別に割り当てられます。
DOM (Document Object Model)
DOMは、HTMLやXMLドキュメントの論理構造をツリー形式で表現するためのプログラミングインターフェースです。WebブラウザはHTMLドキュメントを解析してDOMツリーを構築し、JavaScriptなどのスクリプト言語からこのツリーを操作することで、Webページのコンテンツ、構造、スタイルを動的に変更できます。DOMはW3Cによって標準化されており、appendChild
, removeChild
, insertBefore
などのメソッドは、DOMツリーのノードを操作するための基本的なAPIとして広く認識されています。
Foster Parenting (フォスターペアレンティング)
HTML5のパーシングアルゴリズムにおける「フォスターペアレンティング」は、特定の状況下でHTML要素が通常の親子関係のルールに従わず、別の親ノードに「里子」として追加されるメカニズムです。これは主に、<table>
要素の内部で不正なマークアップ(例: <table><div>...</div></table>
)が検出された場合に発生します。ブラウザはエラーを修正しようとし、div
のようなブロック要素を table
の外に移動させたり、table
の直前の兄弟要素として配置したりします。このメカニズムは、HTMLの堅牢なエラー回復処理の一部であり、不正なHTMLでも可能な限りレンダリングを試みるために重要です。
技術的詳細
このコミットの核となる技術的変更は、Node
構造体の Child
フィールドが []*Node
(スライス) から、FirstChild
, LastChild
, PrevSibling
, NextSibling
といったポインタを持つ双方向連結リスト形式に変更された点です。
変更前 (Node
構造体の一部):
type Node struct {
Parent *Node
Child []*Node // 子ノードをスライスで保持
// ...
}
// Add adds a node as a child of n.
func (n *Node) Add(child *Node) {
if child.Parent != nil {
panic("html: Node.Add called for a child Node that already has a parent")
}
child.Parent = n
n.Child = append(n.Child, child) // スライスへの追加
}
// Remove removes a node as a child of n.
func (n *Node) Remove(child *Node) {
if child.Parent == n {
child.Parent = nil
for i, m := range n.Child {
if m == child {
copy(n.Child[i:], n.Child[i+1:]) // スライス要素の移動
j := len(n.Child) - 1
n.Child[j] = nil
n.Child = n.Child[:j]
return
}
}
}
panic("html: Node.Remove called for a non-child Node")
}
変更後 (Node
構造体の一部):
type Node struct {
Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node // 連結リストのポインタ
// ...
}
// InsertBefore inserts newChild as a child of n, immediately before oldChild.
func (n *Node) InsertBefore(newChild, oldChild *Node) {
// ... ポインタ操作による挿入ロジック ...
}
// AppendChild adds a node c as a child of n.
func (n *Node) AppendChild(c *Node) {
// ... ポインタ操作による追加ロジック ...
}
// RemoveChild removes a node c that is a child of n.
func (n *Node) RemoveChild(c *Node) {
// ... ポインタ操作による削除ロジック ...
}
この変更により、Node.Add
と Node.Remove
はそれぞれ Node.AppendChild
と Node.RemoveChild
にリネームされ、内部実装がスライス操作からポインタ操作に変わりました。
パフォーマンスへの影響:
- CPU使用率の削減: スライスでの要素の追加・削除は、内部配列の再割り当てや要素のコピーを伴うため、CPUサイクルを消費します。連結リストではポインタの付け替えのみで済むため、これらのオーバーヘッドがなくなります。特にHTMLパーシングのように、ツリー構造が頻繁に構築・変更される処理では、この差が顕著に現れます。
- メモリ割り当ての削減: スライスが容量不足になった際に発生する新しい配列の割り当てとコピーが不要になります。コミットメッセージにある
mallocs per iteration
の削減は、この効果を直接的に示しています。メモリ割り当てが減ることで、ガベージコレクションの頻度も減少し、アプリケーション全体の応答性が向上します。 - キャッシュ効率: 一般的に、スライスのような連続したメモリ領域はCPUキャッシュに乗りやすく、アクセスが高速になる傾向があります。一方、連結リストはメモリが分散しているため、キャッシュミスが発生しやすくなる可能性があります。しかし、HTMLノードのようなツリー構造では、ノード間の参照が頻繁に発生するため、必ずしもスライスが有利とは限りません。このコミットのベンチマーク結果は、連結リストへの変更が全体としてパフォーマンス向上に寄与したことを示しており、メモリ割り当ての削減とCPUオーバーヘッドの低減がキャッシュ効率の潜在的なデメリットを上回ったと考えられます。
APIの変更:
Node.Add(child *Node)
->Node.AppendChild(c *Node)
Node.Remove(child *Node)
->Node.RemoveChild(c *Node)
- 新たに
Node.InsertBefore(newChild, oldChild *Node)
が追加されました。これはDOMのinsertBefore
メソッドに相当し、既存の子ノードの前に新しい子ノードを挿入する機能を提供します。
これらのAPI変更は、DOMの標準的な命名規則と操作パターンに合わせることで、開発者にとってより自然で予測可能なインターフェースを提供します。
コアとなるコードの変更箇所
主な変更は以下のファイルに集中しています。
src/pkg/exp/html/node.go
:Node
構造体の定義と、子ノード操作メソッド(Add
,Remove
の削除とInsertBefore
,AppendChild
,RemoveChild
の追加)の実装。reparentChildren
関数も連結リスト操作に更新されています。src/pkg/exp/html/node_test.go
: 新たに作成されたテストファイルで、ノードの親子・兄弟関係の整合性をチェックするcheckTreeConsistency
およびcheckNodeConsistency
関数が追加されています。これは連結リストのポインタ操作が正しく行われていることを検証するために非常に重要です。src/pkg/exp/html/parse.go
: HTMLパーサーのロジックが、変更されたNode
の子ノード操作API(Add
からAppendChild
など)を使用するように更新されています。特にaddChild
,fosterParent
,addText
などの関数で変更が見られます。src/pkg/exp/html/parse_test.go
: パーサーのテストコードが、新しいAPIと整合性チェックを使用するように更新されています。src/pkg/exp/html/render.go
: HTMLレンダラーが、ノードの子要素をイテレートする際にスライスではなく連結リストのポインタ(FirstChild
,NextSibling
)を使用するように変更されています。src/pkg/exp/html/render_test.go
: レンダラーのテストコードが、新しいノード構造に合わせて更新されています。
コアとなるコードの解説
src/pkg/exp/html/node.go
-
Node
構造体の変更:type Node struct { Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node Type NodeType DataAtom atom.Atom Data string Attr []Attribute }
Child []*Node
が削除され、FirstChild
,LastChild
,PrevSibling
,NextSibling
といったポインタが追加されました。これにより、各ノードは自身の子の最初と最後のノード、そして自身の前後の兄弟ノードへの参照を持つことになります。 -
InsertBefore
メソッド: 新しい子ノードnewChild
を、既存の子ノードoldChild
の直前に挿入します。oldChild
がnil
の場合は、newChild
を末尾に追加します。このメソッドは、連結リストのポインタを適切に更新することで、O(1)で挿入操作を実現します。 -
AppendChild
メソッド: 新しい子ノードc
を、現在のノードn
の子リストの末尾に追加します。InsertBefore
と同様に、ポインタ操作のみで行われます。 -
RemoveChild
メソッド: 子ノードc
を現在のノードn
の子リストから削除します。削除後、c
は親も兄弟も持たない状態になります。これもポインタ操作のみでO(1)で実行されます。 -
reparentChildren
関数:src
ノードの全ての子ノードをdst
ノードの子として再親付けする関数です。変更前はスライス操作(append
とcopy
)で行われていましたが、変更後はsrc.RemoveChild(child)
とdst.AppendChild(child)
をループ内で呼び出すことで、連結リストの操作に置き換えられています。これにより、効率的なノードの移動が可能になります。
src/pkg/exp/html/node_test.go
checkTreeConsistency
およびcheckNodeConsistency
: これらの関数は、ノードツリー全体の整合性、特に親子・兄弟関係のポインタが正しく設定されているかを検証するためのものです。無限ループの検出、親子の不一致、兄弟関係の不整合、重複する子ノードなどをチェックします。このような低レベルのデータ構造変更においては、テストによる厳密な整合性チェックが不可欠です。
src/pkg/exp/html/parse.go
および src/pkg/exp/html/render.go
これらのファイルでは、HTMLの解析とレンダリングのロジックが、Node
構造体の変更と新しいAPIに合わせて更新されています。例えば、子ノードの追加は p.top().Add(n)
から p.top().AppendChild(n)
に変更され、子ノードのイテレーションは for _, c := range n.Child
から for c := n.FirstChild; c != nil; c = c.NextSibling
に変更されています。これにより、パーサーとレンダラーは新しい連結リストベースのノード構造を正しく利用できるようになります。
関連リンク
- Go言語の
exp/html
パッケージ (現在のgolang.org/x/net/html
): https://pkg.go.dev/golang.org/x/net/html - Document Object Model (DOM) の概要: https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model
- HTML5 パーシングアルゴリズム (W3C勧告): https://html.spec.whatwg.org/multipage/parsing.html (特に "foster parenting" のセクション)
参考にした情報源リンク
- Go Slices: usage and internals: https://go.dev/blog/slices
- Linked List (Wikipedia): https://ja.wikipedia.org/wiki/%E9%80%A3%E7%B5%90%E3%83%AA%E3%82%B9%E3%83%88
- HTML Standard - 13.2.6.2 The "in body" insertion mode (foster parenting): https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting
- Go CL 6495061 (Gerrit Code Review): https://go-review.googlesource.com/c/6495061 (これはコミットメッセージに記載されているCLへのリンクであり、詳細な変更内容やレビューコメントが含まれています。)
- GoDoc for
exp/html
(当時のドキュメント): https://godoc.org/exp/html (現在はgolang.org/x/net/html
にリダイレクトされる可能性があります) - Go言語のメモリ管理とガベージコレクションに関する一般的な情報源 (例: Goの公式ドキュメントやブログ記事)
- データ構造とアルゴリズムに関する一般的な書籍やオンラインリソース (スライスと連結リストの比較など)
- Webブラウザのレンダリングエンジンに関する情報 (DOMツリーの構築プロセスなど)
- HTML5仕様書 (W3CまたはWHATWG)
- Go言語のベンチマークに関するドキュメント