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

[インデックス 17093] ファイルの概要

このコミットは、Go言語の標準ライブラリ encoding/xml パッケージにおけるXMLマーシャリングの挙動を修正するものです。具体的には、構造体のフィールドがポインタ型であり、かつXML属性としてマーシャリングされる場合に、そのポインタが指す値が正しく処理されるように改善されています。特に、omitemptyタグが指定されている場合や、ポインタが指す値が空でない場合に、適切にXML属性として出力されるようになります。

コミット

commit 547f1a6fe7915193d6c28dac21648f08e2f67bd9
Author: Dmitriy Shelenin <deemok@googlemail.com>
Date:   Thu Aug 8 10:40:51 2013 -0700

    encoding/xml: allow attributes stored in pointers to be marshaled.
    
    Fixes #5334.
    
    R=golang-dev, dave, rsc
    CC=golang-dev
    https://golang.org/cl/8653047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/547f1a6fe7915193d6c28dac21648f08e2f67bd9

元コミット内容

このコミットの目的は、「ポインタに格納された属性がマーシャリングされることを許可する」ことです。これは、Goのencoding/xmlパッケージが、構造体のフィールドがポインタ型であるXML属性を正しく処理できないというバグ(Issue 5334)を修正します。

変更の背景

この変更は、Goのencoding/xmlパッケージが抱えていた特定のバグ、Issue 5334を修正するために導入されました。このバグは、Goの構造体からXMLを生成する(マーシャリングする)際に、XML属性として扱われるフィールドがポインタ型である場合に発生していました。

具体的には、以下のような問題がありました。

  1. omitemptyタグの誤った挙動: 構造体のフィールドがポインタ型で、かつxml:",omitempty"タグが指定されている場合、ポインタがnilでないにもかかわらず、そのポインタが指す値が「空」と判断されると、属性が出力されないという問題がありました。例えば、*string型のフィールドが空文字列""を指している場合、omitemptyが指定されていると属性が省略されてしまっていました。しかし、ポインタ自体はnilではないため、属性は出力されるべきでした。
  2. ポインタのデリファレンス不足: encoding/xmlパッケージのマーシャリングロジックが、ポインタ型のフィールドをXML属性として処理する際に、ポインタのデリファレンス(ポインタが指す実際の値へのアクセス)を適切に行っていませんでした。このため、ポインタが指す値ではなく、ポインタ自体が持つアドレス値のようなものが処理対象となり、期待通りのXML出力が得られませんでした。

これらの問題により、開発者はポインタ型のフィールドをXML属性として柔軟に利用することができず、不便を強いられていました。このコミットは、これらの問題を解決し、encoding/xmlパッケージの堅牢性と使いやすさを向上させることを目的としています。

前提知識の解説

このコミットの理解には、以下のGo言語およびXMLに関する知識が役立ちます。

  1. Go言語のencoding/xmlパッケージ:

    • Go言語でXMLデータを扱うための標準ライブラリです。
    • マーシャリング (Marshaling): Goの構造体(struct)のデータをXML形式のバイト列に変換するプロセスを指します。xml.Marshal関数がこれを行います。
    • アンマーシャリング (Unmarshaling): XML形式のバイト列をGoの構造体に変換するプロセスを指します。xml.Unmarshal関数がこれを行います。
    • 構造体タグ (Struct Tags): 構造体のフィールドに付与されるメタデータです。xml:"name,attr"のように記述され、XML要素名、属性、またはその他のマーシャリング/アンマーシャリングの挙動を制御します。
      • xml:"name": XML要素の名前を指定します。
      • xml:"name,attr": フィールドがXML属性として扱われることを示します。
      • xml:",omitempty": フィールドの値がGoの「ゼロ値」(数値の0、文字列の""、スライスのnilなど)である場合に、XML出力からその要素または属性を省略することを示します。
      • xml:",chardata": フィールドがXML要素の文字データ(内部テキスト)として扱われることを示します。
  2. Go言語のreflectパッケージ:

    • Goのランタイムリフレクション機能を提供するパッケージです。プログラムの実行中に型情報や値情報を動的に検査・操作することを可能にします。
    • reflect.Value: Goの変数の値を表す型です。リフレクション操作の多くはこの型を通じて行われます。
    • reflect.Kind: reflect.Valueが表す値の具体的な種類(例: reflect.Int, reflect.String, reflect.Ptr, reflect.Structなど)を示します。
    • reflect.Ptr: 値がポインタであることを示すreflect.Kindの定数です。
    • reflect.Elem(): reflect.Valueがポインタの場合、そのポインタが指す要素のreflect.Valueを返します。ポインタをデリファレンスする際に使用されます。ポインタでない場合はパニックを起こします。
    • isEmptyValue(reflect.Value): encoding/xmlパッケージ内部で使われるヘルパー関数で、あるreflect.ValueがGoのゼロ値であるかどうかを判定します。この関数は、omitemptyタグの挙動を決定する際に重要です。
  3. XML属性:

    • XML要素の開始タグ内に記述される、要素に関する追加情報を提供するキーと値のペアです。例: <element attribute_name="attribute_value">

これらの知識を前提として、コミットの変更点とそれが解決する問題について深く掘り下げていきます。

技術的詳細

このコミットの技術的詳細は、encoding/xmlパッケージのマーシャリングロジック、特にmarshal.go内のprinter構造体のmarshalValueメソッドとmarshalStructメソッドにおける変更に集約されます。

marshalValueメソッドの変更点

marshalValueメソッドは、個々のフィールドの値をXMLにマーシャリングする役割を担っています。このメソッドにおける主要な変更点は、XML属性として扱われるフィールド(finfo.flagsfAttrフラグが立っているフィールド)の処理ロジックです。

変更前は、omitemptyが指定されている属性フィールドに対しては、isEmptyValue(fv)trueの場合にその属性をスキップしていました。しかし、ポインタ型のフィールドの場合、ポインタ自体がnilでなくても、そのポインタが指す値が空(例: *string""を指す)であればisEmptyValuetrueを返していました。これにより、ポインタがnilでないにもかかわらず属性が省略されるという問題が発生していました。

変更後のコードは以下のようになります。

// 変更前:
// if finfo.flags&fOmitEmpty != 0 && isEmptyValue(fv) {
//     continue
// }

// 変更後:
if (finfo.flags&fOmitEmpty != 0 || fv.Kind() == reflect.Ptr) && isEmptyValue(fv) {
    continue
}

この変更により、omitemptyが指定されているか、またはフィールドがポインタ型である場合にisEmptyValue(fv)trueであればスキップするという条件になりました。一見すると、ポインタ型の場合に常にisEmptyValueが評価されるように見えますが、これはポインタがnilでない場合にisEmptyValuetrueを返すケース(例: *string""を指す)を考慮したものです。

さらに重要な変更は、属性値をマーシャリングする直前に行われるポインタのデリファレンスです。

// 追加されたコード:
// Handle pointer values by following the pointer,
// Pointer is known to be non-nil because we called isEmptyValue above.
if fv.Kind() == reflect.Ptr {
    fv = fv.Elem()
}
if err := p.marshalSimple(fv.Type(), fv); err != nil {
    return err
}

このコードブロックが追加されたことで、fv(フィールドのreflect.Value)がreflect.Ptr型である場合、fv.Elem()を呼び出してポインタが指す実際の値のreflect.Valueを取得し、それをfvに再代入しています。これにより、p.marshalSimple関数が呼び出される際には、ポインタ自体ではなく、ポインタが指す基底の型の値が渡されるようになります。コメントにあるように、この時点ではisEmptyValueのチェックを通過しているため、ポインタはnilではないことが保証されています。

marshalStructメソッドの変更点

marshalStructメソッドは、構造体全体のマーシャリングを管理し、各フィールドを処理します。ここでの変更は、chardata(XML要素のテキストコンテンツ)として扱われるフィールド、または通常のXML要素として扱われるフィールド(fModefCharDataまたはそれ以外)に対して、ポインタのデリファレンスを追加した点です。

// 追加されたコード:
// Handle pointer values by following the pointer
if vf.Kind() == reflect.Ptr && !isEmptyValue(vf) {
    vf = vf.Elem()
}

この変更は、marshalValueの変更と同様に、フィールドのreflect.Valueがポインタ型であり、かつそのポインタがnilでない(!isEmptyValue(vf))場合に、vf.Elem()を呼び出してポインタをデリファレンスするようにしています。これにより、chardataや通常のXML要素としてマーシャリングされるポインタ型のフィールドも、正しくその指す値が処理されるようになります。

変更の全体的な影響

これらの変更により、encoding/xmlパッケージは、ポインタ型のフィールドがXML属性、XML要素の文字データ、または通常のXML要素として定義されている場合でも、そのポインタが指す実際の値を正確にマーシャリングできるようになりました。特に、omitemptyタグとポインタの組み合わせにおける誤った挙動が修正され、より直感的で期待通りのXML出力が得られるようになります。

コアとなるコードの変更箇所

src/pkg/encoding/xml/marshal.go

--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -271,7 +271,7 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {
 			continue
 		}
 		fv := finfo.value(val)
-		if finfo.flags&fOmitEmpty != 0 && isEmptyValue(fv) {
+		if (finfo.flags&fOmitEmpty != 0 || fv.Kind() == reflect.Ptr) && isEmptyValue(fv) {
 			continue
 		}
 		p.WriteByte(' ')
@@ -285,6 +285,11 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {
 		}
 		p.WriteString(finfo.name)
 		p.WriteString(`="`)
+		// Handle pointer values by following the pointer,
+		// Pointer is known to be non-nil because we called isEmptyValue above.
+		if fv.Kind() == reflect.Ptr {
+			fv = fv.Elem()
+		}
 		if err := p.marshalSimple(fv.Type(), fv); err != nil {
 			return err
 		}
@@ -363,6 +368,10 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {
 			continue
 		}
 		vf := finfo.value(val)
+		// Handle pointer values by following the pointer
+		if vf.Kind() == reflect.Ptr && !isEmptyValue(vf) {
+			vf = vf.Elem()
+		}
 		switch finfo.flags & fMode {
 		case fCharData:
 			var scratch [64]byte

src/pkg/encoding/xml/marshal_test.go

--- a/src/pkg/encoding/xml/marshal_test.go
+++ b/src/pkg/encoding/xml/marshal_test.go
@@ -276,6 +276,25 @@ type Strings struct {
 	X []string `xml:"A>B,omitempty"`
 }
 
+type PointerFieldsTest struct {
+	XMLName  Name    `xml:"dummy"`
+	Name     *string `xml:"name,attr"`
+	Age      *uint   `xml:"age,attr"`
+	Empty    *string `xml:"empty,attr"`
+	Contents *string `xml:",chardata"`
+}
+
+type ChardataEmptyTest struct {
+	XMLName  Name    `xml:"test"`
+	Contents *string `xml:",chardata"`
+}
+
+var (
+	nameAttr     = "Sarah"
+	ageAttr      = uint(12)
+	contentsAttr = "lorem ipsum"
+)
+
 // Unless explicitly stated as such (or *Plain), all of the
 // tests below are two-way tests. When introducing new tests,
 // please try to make them two-way as well to ensure that
@@ -673,6 +692,20 @@ var marshalTests = []struct {
 		ExpectXML: `<OmitAttrTest></OmitAttrTest>`,
 	},
 
+	// pointer fields
+	{
+		Value:       &PointerFieldsTest{Name: &nameAttr, Age: &ageAttr, Contents: &contentsAttr},
+		ExpectXML:   `<dummy name="Sarah" age="12">lorem ipsum</dummy>`,
+		MarshalOnly: true,
+	},
+
+	// empty chardata pointer field
+	{
+		Value:       &ChardataEmptyTest{},
+		ExpectXML:   `<test></test>`,
+		MarshalOnly: true,
+	},
+
 	// omitempty on fields
 	{
 		Value: &OmitFieldTest{

コアとなるコードの解説

src/pkg/encoding/xml/marshal.go の変更点

  1. marshalValue メソッド内の omitempty 条件の修正:

    • 変更前: if finfo.flags&fOmitEmpty != 0 && isEmptyValue(fv)
      • これは、「omitemptyフラグが設定されており、かつ値が空である場合」に属性をスキップするという条件でした。ポインタ型の場合、ポインタがnilでなくても、その指す値が空であればisEmptyValuetrueを返し、属性が誤って省略される問題がありました。
    • 変更後: if (finfo.flags&fOmitEmpty != 0 || fv.Kind() == reflect.Ptr) && isEmptyValue(fv)
      • この修正により、「omitemptyフラグが設定されているか、または値がポインタ型である場合、かつ値が空である場合」に属性をスキップするという条件になりました。
      • この変更の意図は、ポインタ型の場合にisEmptyValuetrueを返すケース(例: *string""を指す)を正しく処理することにあります。ポインタがnilでない限り、omitemptyが指定されていても属性は出力されるべきです。この修正は、ポインタがnilの場合にのみ属性を省略するという、より直感的な挙動を実現します。
  2. marshalValue メソッド内のポインタデリファレンス:

    • 追加されたコード:
      if fv.Kind() == reflect.Ptr {
          fv = fv.Elem()
      }
      
    • このコードは、fv(フィールドのreflect.Value)がポインタ型(reflect.Ptr)である場合に、fv.Elem()を呼び出してポインタが指す実際の値のreflect.Valueを取得し、それをfvに再代入しています。
    • これにより、続くp.marshalSimple(fv.Type(), fv)の呼び出しでは、ポインタ自体ではなく、ポインタが指す基底の型の値がマーシャリングされるようになります。これは、XML属性の値としてポインタのアドレスではなく、実際の文字列や数値などを出力するために不可欠な変更です。
  3. marshalStruct メソッド内のポインタデリファレンス:

    • 追加されたコード:
      if vf.Kind() == reflect.Ptr && !isEmptyValue(vf) {
          vf = vf.Elem()
      }
      
    • この変更は、marshalValueと同様に、構造体のフィールドがポインタ型であり、かつそのポインタがnilでない(!isEmptyValue(vf))場合に、vf.Elem()を呼び出してポインタをデリファレンスするようにしています。
    • この修正は、chardata(XML要素のテキストコンテンツ)や通常のXML要素としてマーシャリングされるポインタ型のフィールドに対しても、正しくその指す値が処理されるようにするために必要です。

src/pkg/encoding/xml/marshal_test.go の変更点

  1. 新しいテスト構造体 PointerFieldsTest の追加:

    type PointerFieldsTest struct {
    	XMLName  Name    `xml:"dummy"`
    	Name     *string `xml:"name,attr"`
    	Age      *uint   `xml:"age,attr"`
    	Empty    *string `xml:"empty,attr"`
    	Contents *string `xml:",chardata"`
    }
    
    • この構造体は、*string*uintといったポインタ型のフィールドをXML属性(name,attr)や文字データ(,chardata)として持つケースをテストするために定義されています。Emptyフィールドは、omitemptyの挙動をテストするために使用されます。
  2. 新しいテスト構造体 ChardataEmptyTest の追加:

    type ChardataEmptyTest struct {
    	XMLName  Name    `xml:"test"`
    	Contents *string `xml:",chardata"`
    }
    
    • この構造体は、ポインタ型のchardataフィールドが空の場合の挙動をテストするために使用されます。
  3. テストデータ nameAttr, ageAttr, contentsAttr の追加:

    • これらの変数は、PointerFieldsTest構造体のポインタフィールドに割り当てるための具体的な値を提供します。
  4. marshalTests に新しいテストケースの追加:

    • ポインタフィールドのテストケース:
      {
      	Value:       &PointerFieldsTest{Name: &nameAttr, Age: &ageAttr, Contents: &contentsAttr},
      	ExpectXML:   `<dummy name="Sarah" age="12">lorem ipsum</dummy>`,
      	MarshalOnly: true,
      },
      
      • このテストケースは、PointerFieldsTestのポインタフィールドに実際の値を設定し、期待されるXML出力が正しく生成されることを検証します。MarshalOnly: trueは、このテストがマーシャリングのみを対象とし、アンマーシャリングは行わないことを示します。
    • 空のchardataポインタフィールドのテストケース:
      {
      	Value:       &ChardataEmptyTest{},
      	ExpectXML:   `<test></test>`,
      	MarshalOnly: true,
      },
      
      • このテストケースは、ChardataEmptyTestContentsフィールドがポインタ型で、かつnilである場合に、空のXML要素が生成されることを検証します。これは、omitemptyの挙動がポインタ型に対して正しく適用されることを確認するものです。

これらのテストケースは、コミットによって修正された問題が実際に解決され、ポインタ型のフィールドがXML属性や文字データとして正しくマーシャリングされることを保証します。

関連リンク

参考にした情報源リンク