[インデックス 11322] ファイルの概要
このコミットは、Go言語の encoding/xml
パッケージにおける []byte
型の扱いを改善することを目的としています。具体的には、XML属性における []byte
のマーシャリング(Goのデータ構造からXMLへの変換)と、名前付き []byte
型の一般的なマーシャリングに関するバグが修正されました。また、XML要素が []byte
フィールドにマッピングされた際に、たとえその要素が空であっても、当該フィールドが nil
にならないように挙動が変更されました。さらに、*struct{}
型のフィールドがXML要素の存在テストに正しく機能することを確認するためのテストが追加されましたが、これに関するロジックの変更はありません。
コミット
commit 57007fe12b59fdc61027e5f8cba17444485ca32f
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date: Mon Jan 23 00:50:05 2012 -0200
encoding/xml: improve []byte handling
Marshalling of []byte in attributes and the general
marshalling of named []byte types was fixed.
A []byte field also won't be nil if an XML element
was mapped to it, even if the element is empty.
Tests were introduced to make sure that *struct{}
fields works correctly for element presence testing.
No changes to the logic made in that regard.
R=rsc
CC=golang-dev
https://golang.org/cl/5539070
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/57007fe12b59fdc61027e5f8cba17444485ca32f
元コミット内容
encoding/xml
: []byte
のハンドリングを改善
属性内の []byte
のマーシャリングと、名前付き []byte
型の一般的なマーシャリングが修正されました。
XML要素が []byte
型のフィールドにマッピングされた場合、その要素が空であっても []byte
フィールドは nil
になりません。
*struct{}
フィールドが要素の存在テストに正しく機能することを確認するためのテストが導入されました。この点に関するロジックの変更はありません。
変更の背景
このコミットの背景には、Go言語の encoding/xml
パッケージがXMLデータのマーシャリング(Goの構造体をXMLに変換)およびアンマーシャリング(XMLをGoの構造体に変換)を行う際に、特定のシナリオで []byte
型のデータが期待通りに扱われないという問題がありました。
具体的には、以下の点が課題となっていました。
- XML属性における
[]byte
のマーシャリングの不備: 構造体のフィールドが[]byte
型であり、かつXML属性としてマーシャリングされるべき場合、その変換が正しく行われない可能性がありました。 - 名前付き
[]byte
型のマーシャリングの不備:type MyBytes []byte
のように、[]byte
を基底とするカスタム型が定義された場合、その型の値がXMLに正しくマーシャリングされないケースがありました。 - 空のXML要素と
[]byte
フィールドのマッピング: XMLからGoの構造体へアンマーシャリングする際、空のXML要素(例:<Data></Data>
)が[]byte
型のフィールドにマッピングされると、そのフィールドがnil
になってしまうことがありました。しかし、Goの慣習では、要素が存在する限り、たとえ内容が空であってもスライスはnil
ではなく空のスライス([]byte{}
)であるべきです。これは、要素の「存在」をnil
かどうかで判断するロジックに影響を与える可能性があります。 *struct{}
を用いた要素存在テストの明確化: XML要素の存在をテストするために*struct{}
型のフィールドを使用するパターンがありましたが、その挙動が明確にテストされていませんでした。このコミットでは、このパターンが期待通りに機能することを確認するためのテストが追加されました。
これらの問題を解決し、encoding/xml
パッケージの堅牢性と予測可能性を高めることが、このコミットの主な動機となっています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびXMLに関する基本的な知識が必要です。
-
Go言語の
encoding/xml
パッケージ:- Go標準ライブラリの一部で、Goの構造体とXMLドキュメントの間でデータを変換(マーシャリングとアンマーシャリング)するための機能を提供します。
- マーシャリング (Marshalling): Goのデータ構造(通常は構造体)をXML形式のバイト列に変換するプロセスです。
xml.Marshal
関数がこれを行います。 - アンマーシャリング (Unmarshalling): XML形式のバイト列をGoのデータ構造に変換するプロセスです。
xml.Unmarshal
関数がこれを行います。 - XMLタグ: 構造体のフィールドに付与されるタグ(例:
xml:"name,attr"
)で、XML要素名、属性、またはその他のマーシャリング/アンマーシャリングの挙動を制御します。xml:"name"
: XML要素の名前を指定します。xml:",attr"
: フィールドがXML要素ではなくXML属性として扱われることを示します。xml:",chardata"
: フィールドがXML要素の文字データとして扱われることを示します。xml:",innerxml"
: フィールドがXML要素の内部の生XMLとして扱われることを示します。xml:",omitempty"
: フィールドがその型のゼロ値である場合、XML出力から省略されることを示します。xml:"-"
: フィールドがXMLマーシャリング/アンマーシャリングの対象外であることを示します。
-
[]byte
型:- Go言語におけるバイトスライス型です。バイナリデータやUTF-8エンコードされた文字列を扱う際によく使用されます。
- XMLの文脈では、要素のテキスト内容や属性値がバイト列として表現される場合に
[]byte
にマッピングされることがあります。 - Goのスライスは参照型であり、
nil
はスライスが何も参照していない状態(長さも容量も0)を示します。一方、[]byte{}
は長さ0の空のスライスであり、nil
ではありません。この違いは、要素の存在をチェックする際に重要になります。
-
XML属性:
- XML要素の開始タグ内に記述される
name="value"
形式のデータです。要素の追加情報を提供します。 - 例:
<element attribute="value">...</element>
- XML要素の開始タグ内に記述される
-
*struct{}
型と要素の存在テスト:- Goでは、XML要素の存在をテストする一般的なイディオムとして、構造体内に
*struct{}
型のフィールドを定義することがあります。 - 例:
type MyStruct { Exists *struct{} }
- XMLに
<Exists></Exists>
のような要素が存在する場合、アンマーシャリング後にExists
フィールドはnil
ではないポインタ(&struct{}{}
)になります。要素が存在しない場合、Exists
フィールドはnil
のままです。これにより、XML要素の有無を簡単にチェックできます。
- Goでは、XML要素の存在をテストする一般的なイディオムとして、構造体内に
これらの概念を理解することで、コミットが解決しようとしている具体的な問題と、その解決策がどのように実装されているかを深く把握することができます。
技術的詳細
このコミットは、encoding/xml
パッケージ内の marshal.go
、marshal_test.go
、read.go
の3つのファイルにわたる変更を通じて、[]byte
型のハンドリングを改善しています。
marshal.go
の変更点
marshal.go
は、Goの構造体をXMLにマーシャリングするロジックを含んでいます。主な変更点は以下の通りです。
-
属性のマーシャリングロジックの改善:
- 変更前は、属性の値を文字列として扱い、
fmt.Sprint
で変換していました。また、string
型の属性値が空の場合にのみcontinue
していました。 - 変更後、
marshalValue
関数内で属性を処理する部分がp.marshalSimple(fv.Type(), fv)
を呼び出すように変更されました。これにより、[]byte
型を含む様々な型の属性値がmarshalSimple
関数によって適切に処理されるようになります。 reflect.String
,reflect.Array
,reflect.Slice
型の属性値がfv.Len() == 0
(長さが0) の場合にcontinue
するロジックが追加されました。これは、空の文字列やバイトスライスを属性として出力しないための最適化です。- 属性の出力ロジックが
p.WriteByte(' ')
,p.WriteString(finfo.name)
,p.WriteString(
=")
,p.marshalSimple(...)
,p.WriteByte('"')
のように再構築され、より汎用的なmarshalSimple
関数が利用されるようになりました。
- 変更前は、属性の値を文字列として扱い、
-
marshalSimple
関数の導入と役割:- 以前は
marshalValue
関数内に直接記述されていた、プリミティブ型(int
,string
,bool
など)や[]byte
型の値をXML文字データとして出力するロジックが、新しく導入されたmarshalSimple
関数に切り出されました。 - この関数は、
reflect.Value
を受け取り、その型に応じて適切なXML表現をprinter
に書き込みます。 - 特に
reflect.Slice
のケースでは、以前はval.Interface().([]byte)
で[]byte
にキャストしていましたが、変更後はval.Bytes()
を直接呼び出すことで、より効率的かつ安全にバイトスライスを取得しています。これにより、名前付き[]byte
型(例:type MyBytes []byte
)も正しく処理されるようになります。
- 以前は
これらの変更により、XML属性における []byte
のマーシャリングと、名前付き []byte
型のマーシャリングが修正され、より堅牢なXML出力が可能になりました。
read.go
の変更点
read.go
は、XMLをGoの構造体にアンマーシャリングするロジックを含んでいます。主な変更点は copyValue
関数の挙動です。
[]byte
フィールドのnil
回避:copyValue
関数は、XMLから読み取ったバイト列をGoの構造体のフィールドにコピーする役割を担っています。- 変更前は、
reflect.Slice
のケースでt.Set(reflect.ValueOf(src))
を直接呼び出していました。この場合、src
が空のバイトスライス([]byte{}
)であっても、t.Set
がnil
を設定してしまう可能性がありました。 - 変更後、
reflect.Slice
のケースに以下のロジックが追加されました。if len(src) == 0 { // non-nil to flag presence src = []byte{} } t.SetBytes(src)
- この変更により、XML要素が空であっても(
src
の長さが0)、[]byte
フィールドはnil
ではなく、常に空のバイトスライス([]byte{}
)が設定されるようになりました。これは、XML要素の存在をnil
かどうかで判断する際に、空の要素も「存在する」と見なされるようにするための重要な修正です。
marshal_test.go
の変更点
marshal_test.go
には、上記の変更が意図通りに機能することを確認するための新しいテストケースが追加されました。
-
PresenceTest
構造体の導入:type PresenceTest struct { Exists *struct{} }
という構造体が定義されました。これは、*struct{}
を用いたXML要素の存在テストのイディオムをテストするためのものです。new(struct{})
を持つPresenceTest
と、空のPresenceTest{}
の両方について、期待されるXML出力(それぞれ<PresenceTest><Exists></Exists></PresenceTest>
と<PresenceTest></PresenceTest>
)がテストされています。これにより、*struct{}
フィールドが要素の有無を正しく反映することを確認しています。
-
MyBytes
型とData
構造体の導入:type MyBytes []byte
という名前付き[]byte
型が定義されました。type Data struct { Bytes []byte; Attr []byte
xml:",attr"; Custom MyBytes }
という構造体が定義され、[]byte
フィールド、属性としての[]byte
フィールド、そして名前付き[]byte
型のフィールドを網羅しています。- 以下のシナリオがテストされています。
- 空の
Data{}
をアンマーシャリングした場合、Bytes
フィールドがnil
ではなく空の[]byte{}
になること。 - 空の
Bytes
,Custom
,Attr
を持つData
をアンマーシャリングした場合、それぞれのフィールドがnil
ではなく空の[]byte{}
になること。 - 値を持つ
Bytes
,Custom
,Attr
を持つData
をマーシャリングした場合、期待されるXML出力(例:<Data Attr="v"><Bytes>ab</Bytes><Custom>cd</Custom></Data>
)が生成されること。
- 空の
これらのテストケースは、[]byte
の属性マーシャリング、名前付き []byte
型のマーシャリング、および空のXML要素が []byte
フィールドにマッピングされた際の nil
回避の挙動を検証し、コミットの変更が正しく機能していることを保証します。
コアとなるコードの変更箇所
src/pkg/encoding/xml/marshal.go
--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -181,23 +181,43 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {
if finfo.flags&fAttr == 0 {
continue
}
- var str string
- if fv := val.FieldByIndex(finfo.idx); fv.Kind() == reflect.String {
- str = fv.String()
- } else {
- str = fmt.Sprint(fv.Interface())
+ fv := val.FieldByIndex(finfo.idx)
+ switch fv.Kind() {
+ case reflect.String, reflect.Array, reflect.Slice:
+ // TODO: Should we really do this once ,omitempty is in?
+ if fv.Len() == 0 {
+ continue
+ }
}
- if str != "" {
- p.WriteByte(' ')
- p.WriteString(finfo.name)
- p.WriteString(`="`)
- Escape(p, []byte(str))
- p.WriteByte('"')
+ p.WriteByte(' ')
+ p.WriteString(finfo.name)
+ p.WriteString(`="`)
+ if err := p.marshalSimple(fv.Type(), fv); err != nil {
+ return err
}
+ p.WriteByte('"')
}
p.WriteByte('>')
- switch k := val.Kind(); k {
+ if val.Kind() == reflect.Struct {
+ err = p.marshalStruct(tinfo, val)
+ } else {
+ err = p.marshalSimple(typ, val)
+ }
+ if err != nil {
+ return err
+ }
+
+ p.WriteByte('<')
+ p.WriteByte('/')
+ p.WriteString(name)
+ p.WriteByte('>')
+
+ return nil
+}
+
+func (p *printer) marshalSimple(typ reflect.Type, val reflect.Value) error {
+ switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
p.WriteString(strconv.FormatInt(val.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
@@ -205,21 +225,10 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {
case reflect.Float32, reflect.Float64:
p.WriteString(strconv.FormatFloat(val.Float(), 'g', -1, 64))
case reflect.String:
+ // TODO: Add EscapeString.
Escape(p, []byte(val.String()))
case reflect.Bool:
p.WriteString(strconv.FormatBool(val.Bool()))
case reflect.Array:
// will be []byte
- bytes := val.Interface().([]byte)
- Escape(p, bytes)
case reflect.Slice:
// will be []byte
- bytes := val.Interface().([]byte)
- Escape(p, bytes)
- case reflect.Struct:
- if err := p.marshalStruct(tinfo, val); err != nil {
- return err
- }
+ Escape(p, val.Bytes())
default:
return &UnsupportedTypeError{typ}
}
-
- p.WriteByte('<')
- p.WriteByte('/')
- p.WriteString(name)
- p.WriteByte('>')
-
return nil
}
src/pkg/encoding/xml/read.go
--- a/src/pkg/encoding/xml/read.go
+++ b/src/pkg/encoding/xml/read.go
@@ -134,7 +134,7 @@ import (
//
// Unmarshal maps an XML element to a string or []byte by saving the
// concatenation of that element's character data in the string or
-// []byte.
+// []byte. The saved []byte is never nil.
//
// Unmarshal maps an attribute value to a string or []byte by saving
// the value in the string or slice.
@@ -309,14 +309,12 @@ func (p *Parser) unmarshal(val reflect.Value, start *StartElement) error {
case fAttr:
strv := sv.FieldByIndex(finfo.idx)
// Look for attribute.
- val := ""
for _, a := range start.Attr {
if a.Name.Local == finfo.name {
- val = a.Value
+ copyValue(strv, []byte(a.Value))
break
}
}
- copyValue(strv, []byte(val))
case fCharData:
if !saveData.IsValid() {
@@ -473,7 +471,11 @@ func copyValue(dst reflect.Value, src []byte) (err error) {
case reflect.String:
t.SetString(string(src))
case reflect.Slice:
- t.Set(reflect.ValueOf(src))
+ if len(src) == 0 {
+ // non-nil to flag presence
+ src = []byte{}
+ }
+ t.SetBytes(src)
}\
return nil
}
src/pkg/encoding/xml/marshal_test.go
--- a/src/pkg/encoding/xml/marshal_test.go
+++ b/src/pkg/encoding/xml/marshal_test.go
@@ -184,6 +184,18 @@ type RecurseB struct {
B string
}
+type PresenceTest struct {
+ Exists *struct{}
+}
+
+type MyBytes []byte
+
+type Data struct {
+ Bytes []byte
+ Attr []byte `xml:",attr"`
+ Custom MyBytes
+}
+
type Plain struct {
V interface{}
}
@@ -225,6 +237,44 @@ var marshalTests = []struct {
{Value: &Plain{[]int{1, 2, 3}}, ExpectXML: `<Plain><V>1</V><V>2</V><V>3</V></Plain>`},
{Value: &Plain{[3]int{1, 2, 3}}, ExpectXML: `<Plain><V>1</V><V>2</V><V>3</V></Plain>`},
+ // A pointer to struct{} may be used to test for an element's presence.
+ {
+ Value: &PresenceTest{new(struct{})},
+ ExpectXML: `<PresenceTest><Exists></Exists></PresenceTest>`,
+ },
+ {
+ Value: &PresenceTest{},
+ ExpectXML: `<PresenceTest></PresenceTest>`,
+ },
+
+ // A pointer to struct{} may be used to test for an element's presence.
+ {
+ Value: &PresenceTest{new(struct{})},
+ ExpectXML: `<PresenceTest><Exists></Exists></PresenceTest>`,
+ },
+ {
+ Value: &PresenceTest{},
+ ExpectXML: `<PresenceTest></PresenceTest>`,
+ },
+
+ // A []byte field is only nil if the element was not found.
+ {
+ Value: &Data{},
+ ExpectXML: `<Data></Data>`,
+ UnmarshalOnly: true,
+ },
+ {
+ Value: &Data{Bytes: []byte{}, Custom: MyBytes{}, Attr: []byte{}},
+ ExpectXML: `<Data Attr=""><Bytes></Bytes><Custom></Custom></Data>`,
+ UnmarshalOnly: true,
+ },
+
+ // Check that []byte works, including named []byte types.
+ {
+ Value: &Data{Bytes: []byte("ab"), Custom: MyBytes("cd"), Attr: []byte{'v'}},
+ ExpectXML: `<Data Attr="v"><Bytes>ab</Bytes><Custom>cd</Custom></Data>`,
+ },
+
// Test innerxml
{
Value: &SecretAgent{
コアとなるコードの解説
src/pkg/encoding/xml/marshal.go
このファイルでは、Goの構造体をXMLに変換するマーシャリングロジックが変更されています。
-
属性のマーシャリングロジックの変更:
- 以前は、属性の値を
string
に変換し、そのstring
が空でなければ出力するという単純なロジックでした。これは[]byte
型の属性を適切に扱えない可能性がありました。 - 新しいコードでは、属性の値を
fv
(reflect.Value) として取得し、そのKind()
がreflect.String
,reflect.Array
,reflect.Slice
のいずれかであり、かつfv.Len() == 0
(長さが0) の場合は、その属性をスキップするようになりました。これは、空の文字列やバイトスライスを属性として出力しないための改善です。 - 最も重要な変更は、属性の値を
p.marshalSimple(fv.Type(), fv)
を呼び出して処理するようになった点です。これにより、属性の値が[]byte
型であっても、新しく導入されたmarshalSimple
関数によって適切にXMLエスケープされ、出力されるようになります。
- 以前は、属性の値を
-
marshalSimple
関数の導入:- この新しい関数は、
reflect.Value
を受け取り、その値の型に基づいてXML文字データを生成します。 - 以前は
marshalValue
関数内に散らばっていた、int
,string
,bool
,[]byte
などのプリミティブな値のXML出力ロジックがここに集約されました。 - 特に
reflect.Slice
のケースでは、val.Interface().([]byte)
の代わりにval.Bytes()
を使用するようになりました。val.Bytes()
は[]byte
型のスライスを直接返すため、type MyBytes []byte
のような名前付き[]byte
型も正しく処理できるようになります。これにより、[]byte
型のデータがXMLに正しくマーシャリングされることが保証されます。
- この新しい関数は、
src/pkg/encoding/xml/read.go
このファイルでは、XMLをGoの構造体に変換するアンマーシャリングロジックが変更されています。
copyValue
関数の[]byte
処理の改善:copyValue
関数は、XMLから読み取った値をGoの構造体のフィールドにコピーする役割を担っています。- 変更前は、
reflect.Slice
の場合、単にt.Set(reflect.ValueOf(src))
を呼び出していました。このとき、src
が空のバイトスライス([]byte{}
)であっても、Goのreflect
パッケージの挙動によっては、ターゲットのスライスがnil
に設定されてしまう可能性がありました。 - 新しいコードでは、
if len(src) == 0 { src = []byte{} }
という条件が追加されました。これは、もし入力のバイトスライスsrc
が空である場合、明示的に空のバイトスライスリテラル[]byte{}
をsrc
に再代入することを意味します。 - その後の
t.SetBytes(src)
は、この(nil
ではない)空のバイトスライスをターゲットの[]byte
フィールドに設定します。 - この変更により、XML要素が空であっても、対応するGoの
[]byte
フィールドはnil
ではなく、常に空のバイトスライス([]byte{}
)が設定されるようになります。これは、XML要素の存在をnil
かどうかで判断するロジックにおいて、空の要素も「存在する」と見なされるようにするための重要な修正です。
src/pkg/encoding/xml/marshal_test.go
このファイルは、上記の変更が正しく機能することを確認するためのテストケースを追加しています。
-
PresenceTest
のテスト:*struct{}
型のフィールドがXML要素の存在テストにどのように機能するかを検証します。要素が存在する場合(new(struct{})
)、XMLには空のタグが出力され、要素が存在しない場合({}
)、タグは出力されません。これにより、このイディオムがencoding/xml
パッケージで正しくサポートされていることが確認されます。
-
Data
構造体とMyBytes
型のテスト:[]byte
型のフィールド、属性としての[]byte
、そしてtype MyBytes []byte
のような名前付き[]byte
型が、マーシャリングとアンマーシャリングの両方で正しく機能することを検証します。- 特に、空の
[]byte
フィールドがアンマーシャリング後にnil
ではなく空のスライスになること、および値を持つ[]byte
や名前付き[]byte
が属性や要素として正しくXMLに変換されることを確認しています。
これらのコード変更とテストの追加により、encoding/xml
パッケージは []byte
型のデータをより堅牢かつ予測可能に扱うことができるようになりました。
関連リンク
- Go CL (Change List) 5539070: https://golang.org/cl/5539070
参考にした情報源リンク
- Go言語の
encoding/xml
パッケージのドキュメント - Go言語の
reflect
パッケージのドキュメント - XMLの基本概念(要素、属性、文字データ)
- Go言語におけるスライスと
nil
の概念