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

[インデックス 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 型のデータが期待通りに扱われないという問題がありました。

具体的には、以下の点が課題となっていました。

  1. XML属性における []byte のマーシャリングの不備: 構造体のフィールドが []byte 型であり、かつXML属性としてマーシャリングされるべき場合、その変換が正しく行われない可能性がありました。
  2. 名前付き []byte 型のマーシャリングの不備: type MyBytes []byte のように、[]byte を基底とするカスタム型が定義された場合、その型の値がXMLに正しくマーシャリングされないケースがありました。
  3. 空のXML要素と []byte フィールドのマッピング: XMLからGoの構造体へアンマーシャリングする際、空のXML要素(例: <Data></Data>)が []byte 型のフィールドにマッピングされると、そのフィールドが nil になってしまうことがありました。しかし、Goの慣習では、要素が存在する限り、たとえ内容が空であってもスライスは nil ではなく空のスライス([]byte{})であるべきです。これは、要素の「存在」を nil かどうかで判断するロジックに影響を与える可能性があります。
  4. *struct{} を用いた要素存在テストの明確化: XML要素の存在をテストするために *struct{} 型のフィールドを使用するパターンがありましたが、その挙動が明確にテストされていませんでした。このコミットでは、このパターンが期待通りに機能することを確認するためのテストが追加されました。

これらの問題を解決し、encoding/xml パッケージの堅牢性と予測可能性を高めることが、このコミットの主な動機となっています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびXMLに関する基本的な知識が必要です。

  1. 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マーシャリング/アンマーシャリングの対象外であることを示します。
  2. []byte:

    • Go言語におけるバイトスライス型です。バイナリデータやUTF-8エンコードされた文字列を扱う際によく使用されます。
    • XMLの文脈では、要素のテキスト内容や属性値がバイト列として表現される場合に []byte にマッピングされることがあります。
    • Goのスライスは参照型であり、nil はスライスが何も参照していない状態(長さも容量も0)を示します。一方、[]byte{} は長さ0の空のスライスであり、nil ではありません。この違いは、要素の存在をチェックする際に重要になります。
  3. XML属性:

    • XML要素の開始タグ内に記述される name="value" 形式のデータです。要素の追加情報を提供します。
    • 例: <element attribute="value">...</element>
  4. *struct{} 型と要素の存在テスト:

    • Goでは、XML要素の存在をテストする一般的なイディオムとして、構造体内に *struct{} 型のフィールドを定義することがあります。
    • 例: type MyStruct { Exists *struct{} }
    • XMLに <Exists></Exists> のような要素が存在する場合、アンマーシャリング後に Exists フィールドは nil ではないポインタ(&struct{}{})になります。要素が存在しない場合、Exists フィールドは nil のままです。これにより、XML要素の有無を簡単にチェックできます。

これらの概念を理解することで、コミットが解決しようとしている具体的な問題と、その解決策がどのように実装されているかを深く把握することができます。

技術的詳細

このコミットは、encoding/xml パッケージ内の marshal.gomarshal_test.goread.go の3つのファイルにわたる変更を通じて、[]byte 型のハンドリングを改善しています。

marshal.go の変更点

marshal.go は、Goの構造体をXMLにマーシャリングするロジックを含んでいます。主な変更点は以下の通りです。

  1. 属性のマーシャリングロジックの改善:

    • 変更前は、属性の値を文字列として扱い、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 関数が利用されるようになりました。
  2. 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 関数の挙動です。

  1. []byte フィールドの nil 回避:
    • copyValue 関数は、XMLから読み取ったバイト列をGoの構造体のフィールドにコピーする役割を担っています。
    • 変更前は、reflect.Slice のケースで t.Set(reflect.ValueOf(src)) を直接呼び出していました。この場合、src が空のバイトスライス([]byte{})であっても、t.Setnil を設定してしまう可能性がありました。
    • 変更後、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 には、上記の変更が意図通りに機能することを確認するための新しいテストケースが追加されました。

  1. PresenceTest 構造体の導入:

    • type PresenceTest struct { Exists *struct{} } という構造体が定義されました。これは、*struct{} を用いたXML要素の存在テストのイディオムをテストするためのものです。
    • new(struct{}) を持つ PresenceTest と、空の PresenceTest{} の両方について、期待されるXML出力(それぞれ <PresenceTest><Exists></Exists></PresenceTest><PresenceTest></PresenceTest>)がテストされています。これにより、*struct{} フィールドが要素の有無を正しく反映することを確認しています。
  2. 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に変換するマーシャリングロジックが変更されています。

  1. 属性のマーシャリングロジックの変更:

    • 以前は、属性の値を 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エスケープされ、出力されるようになります。
  2. 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の構造体に変換するアンマーシャリングロジックが変更されています。

  1. 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

このファイルは、上記の変更が正しく機能することを確認するためのテストケースを追加しています。

  1. PresenceTest のテスト:

    • *struct{} 型のフィールドがXML要素の存在テストにどのように機能するかを検証します。要素が存在する場合(new(struct{}))、XMLには空のタグが出力され、要素が存在しない場合({})、タグは出力されません。これにより、このイディオムが encoding/xml パッケージで正しくサポートされていることが確認されます。
  2. Data 構造体と MyBytes 型のテスト:

    • []byte 型のフィールド、属性としての []byte、そして type MyBytes []byte のような名前付き []byte 型が、マーシャリングとアンマーシャリングの両方で正しく機能することを検証します。
    • 特に、空の []byte フィールドがアンマーシャリング後に nil ではなく空のスライスになること、および値を持つ []byte や名前付き []byte が属性や要素として正しくXMLに変換されることを確認しています。

これらのコード変更とテストの追加により、encoding/xml パッケージは []byte 型のデータをより堅牢かつ予測可能に扱うことができるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の encoding/xml パッケージのドキュメント
  • Go言語の reflect パッケージのドキュメント
  • XMLの基本概念(要素、属性、文字データ)
  • Go言語におけるスライスと nil の概念