[インデックス 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 []bytexml:",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の概念