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

[インデックス 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ファイル内で行われています。具体的には、rawTokenscookedTokensrawTokensAltEncodingという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言語の公式ドキュメント
  • Go言語のブログ記事(特にスライスに関するもの)
  • GitHubのGoリポジトリのコミット履歴
  • Go言語コミュニティの議論(Stack Overflowなど)