[インデックス 10396] ファイルの概要
このドキュメントは、Go言語の標準ライブラリencoding/xml
パッケージにおけるテストコードの修正に関するコミットa619da9f4af538194a0b31e10e0f77340511fe1f
について、その背景、技術的詳細、および変更内容を包括的に解説します。
コミット
このコミットは、Go言語のencoding/xml
パッケージ内のテストファイルxml_test.go
におけるビルドエラーを修正するものです。具体的には、StartElement
構造体のAttr
フィールドにnil
を直接代入していた箇所を、空のスライス[]Attr{}
に置き換えることで、Go言語におけるスライスの扱いに関する慣用的な記述に合わせ、ビルドが通るように修正しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a619da9f4af538194a0b31e10e0f77340511fe1f
元コミット内容
commit a619da9f4af538194a0b31e10e0f77340511fe1f
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Tue Nov 15 10:28:01 2011 +0900
xml: fix build
empty is already not a nil.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5376098
---\n src/pkg/encoding/xml/xml_test.go | 14 +++++++-------\n 1 file changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/src/pkg/encoding/xml/xml_test.go b/src/pkg/encoding/xml/xml_test.go\nindex 4c2d196d7b..bcb22afde0 100644\n--- a/src/pkg/encoding/xml/xml_test.go\n+++ b/src/pkg/encoding/xml/xml_test.go\n@@ -42,17 +42,17 @@ var rawTokens = []Token{\n \tCharData([]byte(\"World <>\'\\\" 白鵬翔\")),\n \tEndElement{Name{\"\", \"hello\"}},\n \tCharData([]byte(\"\\n \")),\n-\tStartElement{Name{\"\", \"goodbye\"}, nil},\n+\tStartElement{Name{\"\", \"goodbye\"}, []Attr{}},\n \tEndElement{Name{\"\", \"goodbye\"}},\n \tCharData([]byte(\"\\n \")),\n \tStartElement{Name{\"\", \"outer\"}, []Attr{{Name{\"foo\", \"attr\"}, \"value\"}, {Name{\"xmlns\", \"tag\"}, \"ns4\"}}},\n \tCharData([]byte(\"\\n \")),\n-\tStartElement{Name{\"\", \"inner\"}, nil},\n+\tStartElement{Name{\"\", \"inner\"}, []Attr{}},\n \tEndElement{Name{\"\", \"inner\"}},\n \tCharData([]byte(\"\\n \")),\n \tEndElement{Name{\"\", \"outer\"}},\n \tCharData([]byte(\"\\n \")),\n-\tStartElement{Name{\"tag\", \"name\"}, nil},\n+\tStartElement{Name{\"tag\", \"name\"}, []Attr{}},\n \tCharData([]byte(\"\\n \")),\n \tCharData([]byte(\"Some text here.\")),\n \tCharData([]byte(\"\\n \")),\n@@ -76,17 +76,17 @@ var cookedTokens = []Token{\n \tCharData([]byte(\"World <>\'\\\" 白鵬翔\")),\n \tEndElement{Name{\"ns2\", \"hello\"}},\n \tCharData([]byte(\"\\n \")),\n-\tStartElement{Name{\"ns2\", \"goodbye\"}, nil},\n+\tStartElement{Name{\"ns2\", \"goodbye\"}, []Attr{}},\n \tEndElement{Name{\"ns2\", \"goodbye\"}},\n \tCharData([]byte(\"\\n \")),\n \tStartElement{Name{\"ns2\", \"outer\"}, []Attr{{Name{\"ns1\", \"attr\"}, \"value\"}, {Name{\"xmlns\", \"tag\"}, \"ns4\"}}},\n \tCharData([]byte(\"\\n \")),\n-\tStartElement{Name{\"ns2\", \"inner\"}, nil},\n+\tStartElement{Name{\"ns2\", \"inner\"}, []Attr{}},\n \tEndElement{Name{\"ns2\", \"inner\"}},\n \tCharData([]byte(\"\\n \")),\n \tEndElement{Name{\"ns2\", \"outer\"}},\n \tCharData([]byte(\"\\n \")),\n-\tStartElement{Name{\"ns3\", \"name\"}, nil},\n+\tStartElement{Name{\"ns3\", \"name\"}, []Attr{}},\n \tCharData([]byte(\"\\n \")),\n \tCharData([]byte(\"Some text here.\")),\n \tCharData([]byte(\"\\n \")),\n@@ -104,7 +104,7 @@ var rawTokensAltEncoding = []Token{\n \tCharData([]byte(\"\\n\")),\n \tProcInst{\"xml\", []byte(`version=\"1.0\" encoding=\"x-testing-uppercase\"`)},\n \tCharData([]byte(\"\\n\")),\n-\tStartElement{Name{\"\", \"tag\"}, nil},\n+\tStartElement{Name{\"\", \"tag\"}, []Attr{}},\n \tCharData([]byte(\"value\")),\n \tEndElement{Name{\"\", \"tag\"}},\n }\n```
## 変更の背景
このコミットが行われた2011年11月は、Go言語がまだ比較的新しい言語であり、その仕様や標準ライブラリが活発に開発・洗練されていた時期にあたります。特に、スライス(slice)の`nil`と空(empty)の扱いは、Go言語の設計思想において重要な概念であり、初期の段階ではその慣用的な使用方法が確立されつつありました。
このコミットの背景には、`encoding/xml`パッケージのテストコードにおいて、XML要素の属性(attributes)を表すスライスが`nil`として初期化されていたことが挙げられます。Go言語では、`nil`スライスと長さ0の空スライスは異なる概念です。
* **`nil`スライス**: 基底配列へのポインタが`nil`であり、長さと容量が0のスライス。メモリを割り当てていない状態。
* **空スライス**: 基底配列へのポインタが非`nil`であり、長さと容量が0のスライス。メモリは割り当てられているが、要素がない状態。
多くのGoのAPIや慣用的なコードでは、要素がないことを示す場合には`nil`スライスではなく、空スライス(`[]Type{}`や`make([]Type, 0)`)を使用することが推奨されます。これは、`nil`スライスに対しては特定の操作(例: `append`)が問題なく行える一方で、`nil`チェックを省略できるなど、コードの簡潔性や一貫性を保つためです。
このコミットは、おそらく`encoding/xml`パッケージの内部変更や、Goコンパイラの厳格化、あるいはテストフレームワークの更新により、`StartElement`構造体の`Attr`フィールドが`nil`を受け入れなくなったか、`nil`を渡すとビルドエラーが発生するようになったために行われたと考えられます。コミットメッセージの「empty is already not a nil.」という記述は、空のスライスはもはや`nil`とは異なるものとして扱われるべきである、というGo言語の設計思想の明確化を示唆しています。
## 前提知識の解説
### Go言語のスライス (Slice)
Go言語のスライスは、配列のセグメントを参照する軽量なデータ構造です。スライスは、長さ(length)、容量(capacity)、および基底配列へのポインタの3つの要素で構成されます。
* **長さ (Length)**: スライスに含まれる要素の数。
* **容量 (Capacity)**: スライスの基底配列が保持できる要素の総数。
* **ポインタ**: スライスが参照する基底配列の先頭要素へのポインタ。
スライスは動的なサイズ変更が可能であり、Go言語で最も頻繁に使用されるデータ構造の一つです。
### `nil`スライスと空スライス
Go言語において、スライスは`nil`値を取ることができます。`nil`スライスは、基底配列へのポインタが`nil`であり、長さと容量が両方とも0であるスライスです。これは、スライスが何も参照していない状態を示します。
```go
var s []int // s は nil スライス
fmt.Println(s == nil, len(s), cap(s)) // true 0 0
一方、空スライスは、長さと容量が0ですが、基底配列へのポインタはnil
ではありません。これは、スライスが有効な(ただし空の)配列を参照している状態を示します。
s := []int{} // s は空スライス
fmt.Println(s == nil, len(s), cap(s)) // false 0 0
s = make([]int, 0) // s も空スライス
fmt.Println(s == nil, len(s), cap(s)) // false 0 0
Goの慣例では、要素がないことを示す場合には、nil
スライスではなく空スライスを使用することが推奨されます。これは、nil
スライスと空スライスが多くの操作(例: range
ループ、append
)で同じように振る舞う一方で、nil
スライスは特定の文脈で特別な意味を持つ場合があるためです。例えば、JSONエンコーディングでは、nil
スライスはnull
にエンコードされるのに対し、空スライスは[]
(空のJSON配列)にエンコードされます。
encoding/xml
パッケージ
encoding/xml
パッケージは、Go言語でXMLデータをエンコードおよびデコードするための標準ライブラリです。このパッケージは、XMLパーサーとマーシャラーを提供し、Goの構造体とXML要素間のマッピングを可能にします。
xml.Token
: XMLストリーム内の単一のトークン(要素の開始、終了、文字データなど)を表すインターフェース。xml.StartElement
: XML要素の開始タグを表す構造体。要素名と属性のリストを含みます。type StartElement struct { Name Name Attr []Attr }
xml.Attr
: XML属性を表す構造体。属性名と値を含みます。type Attr struct { Name Name Value string }
このコミットでは、StartElement
構造体のAttr
フィールド([]Attr
型)にnil
を代入していた箇所が問題となっていました。
技術的詳細
このコミットの技術的詳細は、Go言語におけるスライスの初期化と、それが構造体のフィールドにどのように影響するかという点に集約されます。
xml_test.go
ファイルでは、XMLパーサーのテストのために、期待されるXMLトークンのシーケンスを定義しています。このシーケンスには、xml.StartElement
型のトークンが含まれており、そのAttr
フィールド(XML属性のリスト)が初期化されていました。
元のコードでは、属性がないStartElement
に対して、Attr
フィールドに直接nil
を代入していました。
// 変更前
tStartElement{Name{"", "goodbye"}, nil},
しかし、Go言語の進化と慣用的なスタイルの確立に伴い、スライスが要素を持たないことを示す場合には、nil
ではなく空のスライス[]Type{}
を使用することが推奨されるようになりました。これは、nil
スライスと空スライスが異なるセマンティクスを持つ場合があるためです。
この変更は、StartElement
構造体のAttr
フィールドが[]Attr
型であるため、nil
を代入することは型としては許容されますが、特定のGoのバージョンやコンパイラの最適化、あるいはリンターのチェックにおいて、nil
スライスが予期しない振る舞いを引き起こす可能性があったか、あるいは単にコードの慣用的なスタイルに合わせるための修正であったと考えられます。
コミットメッセージの「empty is already not a nil.」は、この変更の意図を明確に示しています。つまり、空の属性リストはnil
として表現されるべきではなく、明示的に空のスライスとして表現されるべきである、というGo言語の設計原則が適用された結果です。これにより、コードの意図がより明確になり、将来的な互換性の問題や予期せぬバグを防ぐことができます。
コアとなるコードの変更箇所
変更はsrc/pkg/encoding/xml/xml_test.go
ファイル内で行われています。具体的には、rawTokens
、cookedTokens
、rawTokensAltEncoding
という3つの[]Token
型の変数定義において、xml.StartElement
の初期化時にAttr
フィールドにnil
を渡していた箇所が、[]Attr{}
(空のAttr
スライス)に置き換えられています。
--- a/src/pkg/encoding/xml/xml_test.go
+++ b/src/pkg/encoding/xml/xml_test.go
@@ -42,17 +42,17 @@ var rawTokens = []Token{
CharData([]byte("World <>\'\\\" 白鵬翔")),
EndElement{Name{"", "hello"}},
CharData([]byte("\n ")),
- StartElement{Name{"", "goodbye"}, nil},
+ StartElement{Name{"", "goodbye"}, []Attr{}},
EndElement{Name{"", "goodbye"}},
CharData([]byte("\n ")),
StartElement{Name{"", "outer"}, []Attr{{Name{"foo", "attr"}, "value"}, {Name{"xmlns", "tag"}, "ns4"}}},
CharData([]byte("\n ")),
- StartElement{Name{"", "inner"}, nil},
+ StartElement{Name{"", "inner"}, []Attr{}},
EndElement{Name{"", "inner"}},
CharData([]byte("\n ")),
EndElement{Name{"", "outer"}},
CharData([]byte("\n ")),
- StartElement{Name{"tag", "name"}, nil},
+ StartElement{Name{"tag", "name"}, []Attr{}},
CharData([]byte("\n ")),
CharData([]byte("Some text here.")),
CharData([]byte("\n ")),
@@ -76,17 +76,17 @@ var cookedTokens = []Token{
CharData([]byte("World <>\'\\\" 白鵬翔")),
EndElement{Name{"ns2", "hello"}},
CharData([]byte("\n ")),
- StartElement{Name{"ns2", "goodbye"}, nil},
+ StartElement{Name{"ns2", "goodbye"}, []Attr{}},
EndElement{Name{"ns2", "goodbye"}},
CharData([]byte("\n ")),
StartElement{Name{"ns2", "outer"}, []Attr{{Name{"ns1", "attr"}, "value"}, {Name{"xmlns", "tag"}, "ns4"}}},
CharData([]byte("\n ")),
- StartElement{Name{"ns2", "inner"}, nil},
+ StartElement{Name{"ns2", "inner"}, []Attr{}},
EndElement{Name{"ns2", "inner"}},
CharData([]byte("\n ")),
EndElement{Name{"ns2", "outer"}},
CharData([]byte("\n ")),
- StartElement{Name{"ns3", "name"}, nil},
+ StartElement{Name{"ns3", "name"}, []Attr{}},
CharData([]byte("\n ")),
CharData([]byte("Some text here.")),
CharData([]byte("\n ")),
@@ -104,7 +104,7 @@ var rawTokensAltEncoding = []Token{
CharData([]byte("\n")),
ProcInst{"xml", []byte(`version="1.0" encoding="x-testing-uppercase"`)},
CharData([]byte("\n")),
- StartElement{Name{"", "tag"}, nil},
+ StartElement{Name{"", "tag"}, []Attr{}},
CharData([]byte("value")),
EndElement{Name{"", "tag"}},
}
コアとなるコードの解説
この変更は、Go言語におけるスライスのnil
と空のセマンティクスに関するベストプラクティスを反映したものです。
StartElement
構造体のAttr
フィールドは[]Attr
型であり、これはAttr
型のスライスを意味します。XML要素に属性がない場合、このスライスは空であるべきです。
-
変更前 (
nil
):StartElement{Name{"", "goodbye"}, nil}
この記述は、goodbye
要素に属性がないことを示していますが、Attr
スライスがnil
であることを意味します。Goの内部処理によっては、nil
スライスに対する操作が、空スライスに対する操作とは異なる振る舞いをすることがあります。特に、初期のGoコンパイラやランタイムでは、このようなnil
スライスの扱いが厳密でなかったり、予期せぬパニックを引き起こす可能性があったりしました。 -
変更後 (
[]Attr{}
):StartElement{Name{"", "goodbye"}, []Attr{}}
この記述は、goodbye
要素に属性がないことを明示的に示し、Attr
スライスが長さ0の空スライスであることを意味します。これは、Go言語で「要素がない」状態を表現する際の慣用的な方法であり、より堅牢で予測可能なコードになります。空スライスは有効なスライスであり、nil
チェックなしで安全にlen()
やcap()
を呼び出したり、append()
操作を行ったりすることができます。
この修正により、encoding/xml
パッケージのテストコードがGo言語の最新の慣用的なスタイルと互換性を持つようになり、ビルドエラーが解消されたと考えられます。これは、Go言語の標準ライブラリが、言語自体の進化に合わせて継続的に改善されている良い例と言えます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のSliceについて: https://go.dev/blog/slices-intro
encoding/xml
パッケージのドキュメント: https://pkg.go.dev/encoding/xml- Go言語のコードレビューコメント(CL): https://golang.org/cl/5376098 (コミットメッセージに記載されているCLへのリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のブログ記事(特にスライスに関するもの)
- GitHubのGoリポジトリのコミット履歴
- Go言語コミュニティの議論(Stack Overflowなど)