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

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

このコミットは、Go言語の標準ライブラリ encoding/xml パッケージにおけるバグ修正を目的としています。具体的には、XML属性の値をポインタ型のフィールドにデコードする際に発生していた問題を解決します。これにより、XMLのアンマーシャリング処理がより堅牢になり、ポインタフィールドへの属性値の正確なマッピングが保証されます。

コミット

  • コミットハッシュ: 4730a226ca78a192ee0fa39df76cd9b260772f5d
  • Author: Kamil Kisiel kamil@kamilkisiel.net
  • Date: Fri Jan 18 17:07:34 2013 -0500
  • コミットメッセージ:
    encoding/xml: fix decoding of attributes in to pointer fields.
    Fixes #3719.
    
    R=anacrolix, rsc
    CC=golang-dev
    https://golang.org/cl/7131052
    

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

https://github.com/golang/go/commit/4730a226ca78a192ee0fa39df76cd9b260772f5d

元コミット内容

commit 4730a226ca78a192ee0fa39df76cd9b260772f5d
Author: Kamil Kisiel <kamil@kamilkisiel.net>
Date:   Fri Jan 18 17:07:34 2013 -0500

    encoding/xml: fix decoding of attributes in to pointer fields.
    Fixes #3719.
    
    R=anacrolix, rsc
    CC=golang-dev
    https://golang.org/cl/7131052
---
 src/pkg/encoding/xml/read.go      |  7 +++++++
 src/pkg/encoding/xml/read_test.go | 44 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/src/pkg/encoding/xml/read.go b/src/pkg/encoding/xml/read.go
index 7a06a29b95..6bc23e1226 100644
--- a/src/pkg/encoding/xml/read.go
+++ b/src/pkg/encoding/xml/read.go
@@ -394,6 +394,13 @@ func copyValue(dst reflect.Value, src []byte) (err error) {
 	\treturn err == nil
 	}\n \n+\tif pv := dst; pv.Kind() == reflect.Ptr {\n+\t\tif pv.IsNil() {\n+\t\t\tpv.Set(reflect.New(pv.Type().Elem()))\n+\t\t}\n+\t\tdst = pv.Elem()\n+\t}\n+\n \t// Save accumulated data.\n \tswitch t := dst; t.Kind() {\n \tcase reflect.Invalid:\ndiff --git a/src/pkg/encoding/xml/read_test.go b/src/pkg/encoding/xml/read_test.go
index 8df09b3cce..b45e2f0e61 100644
--- a/src/pkg/encoding/xml/read_test.go
+++ b/src/pkg/encoding/xml/read_test.go
@@ -355,3 +355,47 @@ func TestUnmarshalWithoutNameType(t *testing.T) {\n \t\tt.Fatalf(\"have %v\\nwant %v\", x.Attr, OK)\n \t}\n }\n+\n+func TestUnmarshalAttr(t *testing.T) {\n+\ttype ParamVal struct {\n+\t\tInt int `xml:\"int,attr\"`\n+\t}\n+\n+\ttype ParamPtr struct {\n+\t\tInt *int `xml:\"int,attr\"`\n+\t}\n+\n+\ttype ParamStringPtr struct {\n+\t\tInt *string `xml:\"int,attr\"`\n+\t}\n+\n+\tx := []byte(`<Param int=\"1\" />`)\n+\n+\tp1 := &ParamPtr{}\n+\tif err := Unmarshal(x, p1); err != nil {\n+\t\tt.Fatalf(\"Unmarshal: %s\", err)\n+\t}\n+\tif p1.Int == nil {\n+\t\tt.Fatalf(\"Unmarshal failed in to *int field\")\n+\t} else if *p1.Int != 1 {\n+\t\tt.Fatalf(\"Unmarshal with %s failed:\\nhave %#v,\\n want %#v\", x, p1.Int, 1)\n+\t}\n+\n+\tp2 := &ParamVal{}\n+\tif err := Unmarshal(x, p2); err != nil {\n+\t\tt.Fatalf(\"Unmarshal: %s\", err)\n+\t}\n+\tif p2.Int != 1 {\n+\t\tt.Fatalf(\"Unmarshal with %s failed:\\nhave %#v,\\n want %#v\", x, p2.Int, 1)\n+\t}\n+\n+\tp3 := &ParamStringPtr{}\n+\tif err := Unmarshal(x, p3); err != nil {\n+\t\tt.Fatalf(\"Unmarshal: %s\", err)\n+\t}\n+\tif p3.Int == nil {\n+\t\tt.Fatalf(\"Unmarshal failed in to *string field\")\n+\t} else if *p3.Int != \"1\" {\n+\t\tt.Fatalf(\"Unmarshal with %s failed:\\nhave %#v,\\n want %#v\", x, p3.Int, 1)\n+\t}\n+}\n```

## 変更の背景

このコミットは、Go言語の `encoding/xml` パッケージにおける特定のバグ、Issue 3719「`encoding/xml`: attributes are not decoded into pointer fields」を修正するために行われました。

このバグは、XML要素の属性をGoの構造体のポインタ型フィールドにアンマーシャリングしようとした際に発生していました。具体的には、`xml:"attr"` タグが付けられたフィールドがポインタ型(例: `*int`, `*string`)である場合、`encoding/xml` パッケージは属性値をそのポインタが指す先へ正しくデコードできませんでした。結果として、ポインタフィールドは `nil` のままであったり、期待される値が設定されなかったりする問題がありました。

この問題は、XMLデータをGoの構造体にマッピングする際に、ポインタフィールドを柔軟に扱いたい開発者にとって大きな制約となっていました。例えば、属性が存在しない場合に `nil` を保持し、存在する場合に値を保持するといったセマンティクスを実現できませんでした。この修正により、`encoding/xml` パッケージはより多様なGoの型定義に対応できるようになり、開発者はXML属性をポインタフィールドに安心してデコードできるようになりました。

## 前提知識の解説

### Go言語の `encoding/xml` パッケージ

`encoding/xml` パッケージは、Go言語でXMLデータをエンコード(Goの構造体からXMLへ)およびデコード(XMLからGoの構造体へ)するための機能を提供します。主な関数として `xml.Marshal` と `xml.Unmarshal` があります。

-   **`xml.Unmarshal(data []byte, v interface{}) error`**: XMLデータをGoの構造体にデコードする際に使用されます。この関数は、XML要素や属性を、Goの構造体のフィールドにマッピングします。マッピングは、構造体フィールドに付けられたタグ(例: `xml:"name,attr"`, `xml:"element"`)に基づいて行われます。

### 構造体タグとXML属性

Goの構造体フィールドには、`xml` タグを付けることで、XMLデータとのマッピング方法を制御できます。
-   `xml:"name"`: XML要素の名前を指定します。
-   `xml:"name,attr"`: XML属性の名前を指定します。このタグが付いたフィールドは、親XML要素の属性として扱われます。
-   `xml:",chardata"`: XML要素の文字データ(テキストコンテンツ)をフィールドにマッピングします。
-   `xml:",innerxml"`: XML要素の内部の生XMLをフィールドにマッピングします。

本コミットでは、特に `xml:"name,attr"` タグが付けられたフィールドがポインタ型である場合の挙動が問題となっていました。

### Go言語の `reflect` パッケージ

`reflect` パッケージは、Goのプログラムが実行時に自身の構造を検査(リフレクション)したり、変更したりする機能を提供します。`encoding/xml` のような汎用的なエンコーディング/デコーディングライブラリは、この `reflect` パッケージを多用して、任意のGoの構造体のフィールドに動的にアクセスし、値を設定します。

-   **`reflect.Value`**: Goの任意の型の値を表すリフレクションオブジェクトです。
-   **`reflect.Kind()`**: `reflect.Value` が表す値の基本的な種類(例: `reflect.Int`, `reflect.String`, `reflect.Ptr`, `reflect.Struct` など)を返します。
-   **`reflect.Ptr`**: ポインタ型を表す `reflect.Kind` です。
-   **`reflect.IsNil()`**: ポインタ、インターフェース、マップ、スライス、チャネル、関数などの `reflect.Value` が `nil` であるかどうかを判定します。
-   **`reflect.Type()`**: `reflect.Value` が表す値の型を返します。
-   **`reflect.Type().Elem()`**: ポインタ型の場合、そのポインタが指す要素の型を返します。
-   **`reflect.New(typ reflect.Type)`**: 指定された型 `typ` の新しい値へのポインタを返します。これは、`new(T)` と同等です。
-   **`reflect.Value.Set(x reflect.Value)`**: `reflect.Value` が表す値に、別の `reflect.Value` `x` の値を設定します。この操作は、`reflect.Value` が変更可能(settable)である場合にのみ可能です。
-   **`reflect.Value.Elem()`**: ポインタ型の場合、そのポインタが指す要素の `reflect.Value` を返します。

`encoding/xml` パッケージは、XML属性の値をGoの構造体フィールドに設定する際に、`reflect` パッケージを使ってフィールドの型を調べ、適切な変換と値の設定を行います。

## 技術的詳細

このバグは、`encoding/xml` パッケージ内部の `copyValue` 関数に起因していました。`copyValue` 関数は、XMLから読み取ったバイトスライス `src` の内容を、Goの構造体フィールドを表す `reflect.Value` `dst` にコピーする役割を担っています。

問題は、`dst` がポインタ型(例: `*int`)であるにもかかわらず、そのポインタが `nil` の場合に、新しいインスタンスを割り当ててポインタが指す先を初期化する処理が欠けていた点にありました。

修正前の `copyValue` 関数は、`dst` がポインタ型であるかどうかを直接チェックせず、`dst` が指す先の値に直接データをコピーしようとしていました。しかし、`dst` が `nil` ポインタの場合、その先に値を書き込むことはできません(パニックを引き起こすか、単に値が設定されない)。

このコミットでは、`copyValue` 関数に以下のロジックが追加されました。

1.  `dst` がポインタ型であるか (`pv.Kind() == reflect.Ptr`) をチェックします。
2.  もしポインタ型であれば、そのポインタが `nil` であるか (`pv.IsNil()`) をチェックします。
3.  `nil` であれば、`reflect.New(pv.Type().Elem())` を使って、ポインタが指す先の型の新しいインスタンスを生成し、そのポインタを `pv.Set()` で `dst` に設定します。これにより、`nil` ポインタが有効なメモリを指すようになります。
4.  最後に、`dst = pv.Elem()` を実行して、`dst` をポインタが指す実際の要素の `reflect.Value` に更新します。これにより、後続の `switch t := dst; t.Kind()` ブロックで、ポインタの先の値に正しくデータがコピーされるようになります。

この変更により、`encoding/xml` はXML属性をポインタフィールドにデコードする際に、必要に応じてポインタの指す先を自動的に初期化し、値を正しく設定できるようになりました。

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

`src/pkg/encoding/xml/read.go` の `copyValue` 関数に以下の7行が追加されました。

```diff
--- a/src/pkg/encoding/xml/read.go
+++ b/src/pkg/encoding/xml/read.go
@@ -394,6 +394,13 @@ func copyValue(dst reflect.Value, src []byte) (err error) {
 	\treturn err == nil
 	}\n \n+\tif pv := dst; pv.Kind() == reflect.Ptr {\n+\t\tif pv.IsNil() {\n+\t\t\tpv.Set(reflect.New(pv.Type().Elem()))\n+\t\t}\n+\t\tdst = pv.Elem()\n+\t}\n+\n \t// Save accumulated data.\n \tswitch t := dst; t.Kind() {\
 \tcase reflect.Invalid:\

また、src/pkg/encoding/xml/read_test.goTestUnmarshalAttr という新しいテストケースが追加され、ポインタ型フィールドへの属性デコードが正しく行われることを検証しています。

コアとなるコードの解説

追加されたコードブロックは、copyValue 関数が値のコピーを行う前に、ターゲットの reflect.Value dst がポインタ型であるかどうか、そしてそれが nil であるかどうかをチェックします。

if pv := dst; pv.Kind() == reflect.Ptr {
	if pv.IsNil() {
		pv.Set(reflect.New(pv.Type().Elem()))
	}
	dst = pv.Elem()
}
  1. if pv := dst; pv.Kind() == reflect.Ptr { ... }:

    • dstpv という新しい reflect.Value 変数に代入します。
    • pv.Kind() == reflect.Ptr で、dst がポインタ型であるかをチェックします。この条件が真の場合のみ、ブロック内の処理が実行されます。
  2. if pv.IsNil() { ... }:

    • pv がポインタ型であり、かつそのポインタが nil であるかをチェックします。つまり、まだメモリが割り当てられていない状態です。
  3. pv.Set(reflect.New(pv.Type().Elem())):

    • pv.Type().Elem(): pv がポインタ型なので、このメソッドはポインタが指す先の型(例: *int なら int 型)を返します。
    • reflect.New(...): 指定された型の新しいインスタンスへのポインタを生成します。これは new(T) と同じ効果を持ちます。
    • pv.Set(...): pv(元の dst のポインタ)に、新しく生成されたインスタンスへのポインタを設定します。これにより、nil だったポインタが有効なメモリを指すようになります。
  4. dst = pv.Elem():

    • pv.Elem(): pv が指す先の値(つまり、新しく初期化された intstring の値)の reflect.Value を返します。
    • この行で、dst 変数を、ポインタの先の実際の値の reflect.Value に更新します。これにより、この後の switch ステートメントで、dst が直接その値(例: intstring)として扱われ、src からのデータが正しくコピーされるようになります。

このロジックにより、encoding/xml はXML属性をポインタフィールドにデコードする際に、ポインタが nil であっても適切に初期化し、値を設定できるようになりました。

関連リンク

参考にした情報源リンク

  • Go Issue Tracker (Issue 3719): 上記のリンク
  • Go Code Review (CL 7131052): 上記のリンク
  • Go言語の encoding/xml パッケージのドキュメント: https://pkg.go.dev/encoding/xml
  • Go言語の reflect パッケージのドキュメント: https://pkg.go.dev/reflect
  • Go言語におけるリフレクションの基本概念に関する一般的な情報源 (例: Go by Example: Reflection, A Tour of Go: Reflection)