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

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

このコミットは、Go言語の encoding/xml パッケージにおけるXML名前空間のプレフィックス生成ロジックを改善し、より人間が読みやすいプレフィックスを使用するように変更するものです。また、XML標準で定義されている xml 名前空間プレフィックスを事前に定義することで、XMLのマーシャリング(Goの構造体からXMLへの変換)の正確性と効率性を向上させています。

コミット

commit bdf8bf6adc956719a3a224f32c1ca6e6df77dbac
Author: Russ Cox <rsc@golang.org>
Date:   Wed Mar 13 14:36:42 2013 -0400

    encoding/xml: predefine xml name space prefix
    
    Also change prefix generation to use more human-friendly prefixes.
    
    Fixes #5040.
    
    R=golang-dev, r, bradfitz
    CC=golang-dev
    https://golang.org/cl/7777047

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

https://github.com/golang/go/commit/bdf8bf6adc956719a3a224f32c1ca6e6df77dbac

元コミット内容

encoding/xml: predefine xml name space prefix Also change prefix generation to use more human-friendly prefixes. Fixes #5040.

変更の背景

このコミットの主な背景には、Goの encoding/xml パッケージがXMLをマーシャリングする際に、名前空間プレフィックスの生成方法に課題があったことが挙げられます。

  1. xml 名前空間の扱い: XMLの仕様では、http://www.w3.org/XML/1998/namespace というURIを持つ名前空間は、常に xml というプレフィックスで参照されることが定められています。しかし、以前の実装では、この特別な名前空間に対しても他の名前空間と同様に自動生成されたプレフィックス(例: _1, _2 など)が割り当てられる可能性がありました。これはXMLの標準に準拠しておらず、生成されるXMLの互換性や可読性に問題を引き起こす可能性がありました。
  2. プレフィックスの可読性: 以前の encoding/xml パッケージは、名前空間プレフィックスを _1, _2 のように連番で生成していました。これは機械的な処理には問題ありませんが、人間がXMLドキュメントを読んだりデバッグしたりする際には、どのプレフィックスがどの名前空間に対応しているのかを把握しにくく、可読性が低いという問題がありました。
  3. Issue #5040 の解決: コミットメッセージに Fixes #5040 とあるように、この変更は特定のバグや改善要求(Issue 5040)に対応するものです。このIssueは、おそらく上記の名前空間プレフィックスの生成に関する問題、特に xml 名前空間の不適切な扱いに関するものだったと推測されます。

これらの課題を解決し、より標準に準拠し、かつ人間にとって読みやすいXMLを生成できるようにするために、このコミットが導入されました。

前提知識の解説

このコミットを理解するためには、以下のXML関連の概念とGoの encoding/xml パッケージの基本的な動作について理解しておく必要があります。

XML名前空間 (XML Namespaces)

XML名前空間は、XMLドキュメント内で要素名や属性名の衝突を避けるためのメカニズムです。異なるXML語彙(スキーマ)から要素や属性を組み合わせる際に特に重要になります。

  • URI (Uniform Resource Identifier): 名前空間はURIによって一意に識別されます。このURIは通常、ウェブサイトのURLのような形式ですが、必ずしもそのURIにアクセスできる必要はありません。単なる識別子として機能します。 例: http://www.w3.org/TR/html4/
  • プレフィックス (Prefix): XMLドキュメント内で名前空間に属する要素や属性を示すために、短い文字列のプレフィックスが使用されます。プレフィックスは xmlns: 属性を使ってURIにマッピングされます。 例: <h:table xmlns:h="http://www.w3.org/TR/html4/"> ここで h はプレフィックスで、http://www.w3.org/TR/html4/ にマッピングされています。
  • デフォルト名前空間: プレフィックスなしで定義された名前空間は、その要素とその子要素のデフォルト名前空間となります。属性には適用されません。 例: <table xmlns="http://www.w3.org/TR/html4/">
  • xml 名前空間: http://www.w3.org/XML/1998/namespace というURIを持つ名前空間は、XMLの組み込み属性(例: xml:lang, xml:space)のために予約されており、常に xml というプレフィックスを使用することがXML仕様で定められています。

Go encoding/xml パッケージ

Goの標準ライブラリである encoding/xml パッケージは、Goの構造体とXMLドキュメントの間でデータをマーシャリング(Go構造体 → XML)およびアンマーシャリング(XML → Go構造体)するための機能を提供します。

  • マーシャリング (Marshalling): Goの構造体のフィールドをXML要素や属性に変換するプロセスです。構造体のフィールドタグ(例: xml:"name,attr")を使用して、XML要素名、属性名、名前空間などを指定できます。
  • アンマーシャリング (Unmarshalling): XMLドキュメントをGoの構造体に変換するプロセスです。
  • xml.Name 構造体: XML要素や属性の名前を表す構造体で、Space(名前空間URI)と Local(ローカル名)のフィールドを持ちます。
  • printer 構造体: encoding/xml パッケージ内部でXMLの出力(マーシャリング)を管理するための構造体です。この構造体が、名前空間プレフィックスの生成と管理を担当します。

技術的詳細

このコミットは、主に encoding/xml パッケージのマーシャリング処理における名前空間プレフィックスの生成と管理ロジックを改善しています。

1. printer 構造体の拡張

printer 構造体に以下の2つのマップが追加されました。

  • attrNS map[string]string: プレフィックス(string)から名前空間URI(string)へのマッピングを保持します。これにより、どのプレフィックスがどの名前空間URIに割り当てられているかを追跡できます。
  • attrPrefix map[string]string: 名前空間URI(string)からプレフィックス(string)へのマッピングを保持します。これにより、特定名前空間URIに対する既存のプレフィックスを効率的に検索できます。

これらのマップは、XMLドキュメント内で使用される名前空間プレフィックスの一貫性を保ち、重複を避けるために使用されます。

2. createAttrPrefix 関数の導入

この新しい関数は、与えられた名前空間URIに対して適切なプレフィックスを見つけ、必要であれば新しいプレフィックスを定義します。

  • 既存プレフィックスの再利用: まず p.attrPrefix マップをチェックし、指定されたURIに対応するプレフィックスが既に存在すればそれを再利用します。これにより、同じ名前空間URIに対して複数のプレフィックスが生成されるのを防ぎます。
  • xml 名前空間の特別扱い: http://www.w3.org/XML/1998/namespace というURIが渡された場合、XML仕様に従って常に "xml" というプレフィックスを返します。これは、この名前空間が特別な意味を持つため、他の名前空間とは異なる扱いをする必要があるためです。
  • 人間が読みやすいプレフィックスの生成: 新しいプレフィックスが必要な場合、名前空間URIの最後のパス要素をプレフィックスとして試みます(例: http://www.w3.org/TR/html4/ から html4)。これにより、以前の _1, _2 のような機械的なプレフィックスではなく、名前空間の内容をある程度推測できる、より意味のあるプレフィックスが生成されます。
  • プレフィックスの衝突解決: 生成しようとしたプレフィックスが既に他の名前空間に割り当てられている場合、_ と連番(例: html4_1, html4_2)を付加して一意なプレフィックスを生成します。
  • xmlns: 属性の出力: 新しいプレフィックスが生成された場合、対応する xmlns:prefix="uri" 属性がXML出力ストリームに書き込まれます。

3. deleteAttrPrefix 関数の導入

この関数は、createAttrPrefix で一時的に追加された名前空間プレフィックスを、要素のスコープが終了した際に削除するために使用されます。これは、defer ステートメントと組み合わせて使用され、名前空間の定義がその要素のスコープ内でのみ有効であることを保証します。

4. marshalValue の変更

marshalValue 関数は、Goの構造体フィールドをXML要素にマーシャリングする際に、名前空間プレフィックスの生成に createAttrPrefix を使用するように変更されました。これにより、名前空間を持つ属性や要素のプレフィックスが、新しいロジックに基づいて生成されるようになります。

5. xml.go の変更

  • xmlURL 定数の追加: http://www.w3.org/XML/1998/namespace のURIを xmlURL という定数として定義し、コード全体での一貫性を保ちます。
  • translate 関数の変更: xml プレフィックスが指定された場合、その名前空間URIを xmlURL に変換するように変更されました。これにより、XMLパーサーが xml:lang などの属性を正しく解釈できるようになります。

これらの変更により、encoding/xml パッケージはXML名前空間の扱いにおいて、より堅牢で標準に準拠し、かつ人間にとって読みやすいXMLを生成できるようになりました。

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

src/pkg/encoding/xml/marshal.go

  • printer 構造体に attrNSattrPrefix マップが追加。
  • createAttrPrefix 関数が新規追加(約60行)。
  • deleteAttrPrefix 関数が新規追加(約4行)。
  • marshalValue 関数内で、名前空間プレフィックスの生成ロジックが変更され、createAttrPrefixdeleteAttrPrefix が使用されるようになった。
--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -126,6 +126,70 @@ type printer struct {
 	depth      int
 	indentedIn bool
 	putNewline bool
+	attrNS     map[string]string // map prefix -> name space
+	attrPrefix map[string]string // map name space -> prefix
+}
+
+// createAttrPrefix finds the name space prefix attribute to use for the given name space,
+// defining a new prefix if necessary. It returns the prefix and whether it is new.
+func (p *printer) createAttrPrefix(url string) (prefix string, isNew bool) {
+	if prefix = p.attrPrefix[url]; prefix != "" {
+		return prefix, false
+	}
+
+	// The "http://www.w3.org/XML/1998/namespace" name space is predefined as "xml"
+	// and must be referred to that way.
+	// (The "http://www.w3.org/2000/xmlns/" name space is also predefined as "xmlns",
+	// but users should not be trying to use that one directly - that's our job.)
+	if url == xmlURL {
+		return "xml", false
+	}
+
+	// Need to define a new name space.
+	if p.attrPrefix == nil {
+		p.attrPrefix = make(map[string]string)
+		p.attrNS = make(map[string]string)
+	}
+
+	// Pick a name. We try to use the final element of the path
+	// but fall back to _.
+	prefix = strings.TrimRight(url, "/")
+	if i := strings.LastIndex(prefix, "/"); i >= 0 {
+		prefix = prefix[i+1:]
+	}
+	if prefix == "" || !isName([]byte(prefix)) || strings.Contains(prefix, ":") {
+		prefix = "_"
+	}
+	if strings.HasPrefix(prefix, "xml") {
+		// xmlanything is reserved.
+		prefix = "_" + prefix
+	}
+	if p.attrNS[prefix] != "" {
+		// Name is taken. Find a better one.
+		for p.seq++; ; p.seq++ {
+			if id := prefix + "_" + strconv.Itoa(p.seq); p.attrNS[id] == "" {
+				prefix = id
+				break
+			}
+		}
+	}
+
+	p.attrPrefix[url] = prefix
+	p.attrNS[prefix] = url
+
+	p.WriteString(`xmlns:`)
+	p.WriteString(prefix)
+	p.WriteString(`="`)
+	EscapeText(p, []byte(url))
+	p.WriteString(`" `)
+
+	return prefix, true
+}
+
+// deleteAttrPrefix removes an attribute name space prefix.
+func (p *printer) deleteAttrPrefix(prefix string) {
+	delete(p.attrPrefix, p.attrNS[prefix])
+	delete(p.attrNS, prefix)
 }
 
 // marshalValue writes one or more XML elements representing val.
@@ -212,17 +276,11 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {
 		}
 		p.WriteByte(' ')
 		if finfo.xmlns != "" {
-			p.WriteString("xmlns:")
-			p.seq++
-			id := "_" + strconv.Itoa(p.seq)
-			p.WriteString(id)
-			p.WriteString(`="`)
-			// TODO: EscapeString, to avoid the allocation.
-			if err := EscapeText(p, []byte(finfo.xmlns)); err != nil {
-				return err
+			prefix, created := p.createAttrPrefix(finfo.xmlns)
+			if created {
+				defer p.deleteAttrPrefix(prefix)
 			}
-			p.WriteString(`" `)
-			p.WriteString(id)
+			p.WriteString(prefix)
 			p.WriteByte(':')
 		}
 		p.WriteString(finfo.name)

src/pkg/encoding/xml/read_test.go

  • TAttr 構造体に新しいフィールドが追加され、xml:lang やカスタム名前空間を持つ属性のテストケースが追加された。
  • TestMarshalNSAttr テスト関数が更新され、新しいプレフィックス生成ロジックに対応する期待されるXML出力が変更された。また、マーシャリング後のアンマーシャリングテストも追加された。
--- a/src/pkg/encoding/xml/read_test.go
+++ b/src/pkg/encoding/xml/read_test.go
@@ -503,6 +503,11 @@ type TableAttrs struct {
 type TAttr struct {
 	HTable string `xml:"http://www.w3.org/TR/html4/ table,attr"`
 	FTable string `xml:"http://www.w3schools.com/furniture table,attr"`
+	Lang   string `xml:"http://www.w3.org/XML/1998/namespace lang,attr,omitempty"`
+	Other1 string `xml:"http://golang.org/xml/ other,attr,omitempty"`
+	Other2 string `xml:"http://golang.org/xmlfoo/ other,attr,omitempty"`
+	Other3 string `xml:"http://golang.org/json/ other,attr,omitempty"`
+	Other4 string `xml:"http://golang.org/2/json/ other,attr,omitempty"`
 }
 
 var tableAttrs = []struct {
@@ -514,33 +519,33 @@ var tableAttrs = []struct {
 		xml: `<TableAttrs xmlns:f="http://www.w3schools.com/furniture" xmlns:h="http://www.w3.org/TR/html4/"><TAttr ` +\
 			`h:table="hello" f:table="world" ` +\
 			`/></TableAttrs>`,
-		tab: TableAttrs{TAttr{"hello", "world"}},
+		tab: TableAttrs{TAttr{HTable: "hello", FTable: "world"}},
 	},
 	{
 		xml: `<TableAttrs><TAttr xmlns:f="http://www.w3schools.com/furniture" xmlns:h="http://www.w3.org/TR/html4/" ` +\
 			`h:table="hello" f:table="world" ` +\
 			`/></TableAttrs>`,
-		tab: TableAttrs{TAttr{"hello", "world"}},
+		tab: TableAttrs{TAttr{HTable: "hello", FTable: "world"}},
 	},
 	{
 		xml: `<TableAttrs><TAttr ` +\
 			`h:table="hello" f:table="world" xmlns:f="http://www.w3schools.com/furniture" xmlns:h="http://www.w3.org/TR/html4/" ` +\
 			`/></TableAttrs>`,
-		tab: TableAttrs{TAttr{"hello", "world"}},
+		tab: TableAttrs{TAttr{HTable: "hello", FTable: "world"}},
 	},
 	{
 		// Default space does not apply to attribute names.
 		xml: `<TableAttrs xmlns="http://www.w3schools.com/furniture" xmlns:h="http://www.w3.org/TR/html4/"><TAttr ` +\
 			`h:table="hello" table="world" ` +\
 			`/></TableAttrs>`,
-		tab: TableAttrs{TAttr{"hello", ""}},
+		tab: TableAttrs{TAttr{HTable: "hello", FTable: ""}},
 	},
 	{
 		// Default space does not apply to attribute names.
 		xml: `<TableAttrs xmlns:f="http://www.w3schools.com/furniture"><TAttr xmlns="http://www.w3.org/TR/html4/" ` +\
 			`table="hello" f:table="world" ` +\
 			`/></TableAttrs>`,
-		tab: TableAttrs{TAttr{"", "world"}},
+		tab: TableAttrs{TAttr{HTable: "", FTable: "world"}},
 	},
 	{
 		xml: `<TableAttrs><TAttr ` +\
@@ -553,14 +558,23 @@ var tableAttrs = []struct {
 	xml: `<TableAttrs xmlns:h="http://www.w3.org/TR/html4/"><TAttr ` +\
 		`h:table="hello" table="world" ` +\
 		`/></TableAttrs>`,
-	tab: TableAttrs{TAttr{"hello", ""}},
+	tab: TableAttrs{TAttr{HTable: "hello", FTable: ""}},
 	ns:  "http://www.w3schools.com/furniture",
 	},
 	{
 	xml: `<TableAttrs xmlns:f="http://www.w3schools.com/furniture"><TAttr ` +\
 		`table="hello" f:table="world" ` +\
 		`/></TableAttrs>`,
-	tab: TableAttrs{TAttr{"", "world"}},
+	tab: TableAttrs{TAttr{HTable: "", FTable: "world"}},
 	ns:  "http://www.w3.org/TR/html4/",
 	},
 	{
@@ -596,14 +610,23 @@ func TestUnmarshalNSAttr(t *testing.T) {
 }
 
 func TestMarshalNSAttr(t *testing.T) {
-	dst := TableAttrs{TAttr{"hello", "world"}}
-	data, err := Marshal(&dst)
+	src := TableAttrs{TAttr{"hello", "world", "en_US", "other1", "other2", "other3", "other4"}}
+	data, err := Marshal(&src)
 	if err != nil {
 		t.Fatalf("Marshal: %v", err)
 	}
-	want := `<TableAttrs><TAttr xmlns:_1="http://www.w3.org/TR/html4/" _1:table="hello" xmlns:_2="http://www.w3schools.com/furniture" _2:table="world"></TAttr></TableAttrs>`
+	want := `<TableAttrs><TAttr xmlns:html4="http://www.w3.org/TR/html4/" html4:table="hello" xmlns:furniture="http://www.w3schools.com/furniture" furniture:table="world" xml:lang="en_US" xmlns:_xml="http://golang.org/xml/" _xml:other="other1" xmlns:_xmlfoo="http://golang.org/xmlfoo/" _xmlfoo:other="other2" xmlns:json="http://golang.org/json/" json:other="other3" xmlns:json_1="http://golang.org/2/json/" json_1:other="other4"></TAttr></TableAttrs>`
 	str := string(data)
 	if str != want {
-		t.Errorf("have: %q\nwant: %q\n", str, want)
+		t.Errorf("Marshal:\nhave: %#q\nwant: %#q\n", str, want)
+	}
+
+	var dst TableAttrs
+	if err := Unmarshal(data, &dst); err != nil {
+		t.Errorf("Unmarshal: %v", err)
+	}
+
+	if dst != src {
+		t.Errorf("Unmarshal = %q, want %q", dst, src)
 	}
 }

src/pkg/encoding/xml/xml.go

  • xmlURL 定数が新規追加。
  • translate 関数内で、n.Space == "xml" の場合に n.SpacexmlURL に変換するロジックが追加された。
--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -273,6 +273,8 @@ func (d *Decoder) Token() (t Token, err error) {
 	return
 }
 
+const xmlURL = "http://www.w3.org/XML/1998/namespace"
+
 // Apply name space translation to name n.
 // The default name space (for Space=="")
 // applies only to element names, not to attribute names.
@@ -282,6 +284,8 @@ func (d *Decoder) translate(n *Name, isElementName bool) {
 		return
 	case n.Space == "" && !isElementName:
 		return
+	case n.Space == "xml":
+		n.Space = xmlURL
 	case n.Space == "" && n.Local == "xmlns":
 		return
 	}

コアとなるコードの解説

marshal.go の変更点

  • printer 構造体の強化: attrNSattrPrefix マップは、printer が現在処理しているXMLドキュメントのスコープ内で、どの名前空間URIがどのプレフィックスにマッピングされているかを効率的に管理するために導入されました。これにより、同じ名前空間URIに対して常に同じプレフィックスを使用し、冗長な xmlns 宣言を避けることができます。
  • createAttrPrefix(url string) (prefix string, isNew bool) 関数: この関数は、名前空間URI url に対応するプレフィックスを取得または生成する中心的なロジックです。
    1. 既存プレフィックスのチェック: p.attrPrefix[url] を参照し、既にこのURIに割り当てられたプレフィックスがあればそれを即座に返します。isNewfalse となります。
    2. xml 名前空間の特別処理: url == xmlURL (つまり http://www.w3.org/XML/1998/namespace) の場合、XML仕様に従い "xml" をプレフィックスとして返します。このプレフィックスは常に存在するため isNewfalse です。
    3. 新しいプレフィックスの生成:
      • p.attrPrefixp.attrNSnil の場合、初めて名前空間が定義されるため、マップを初期化します。
      • 人間が読みやすいプレフィックスの試み: strings.TrimRight(url, "/") でURIの末尾のスラッシュを削除し、strings.LastIndex(prefix, "/") で最後のスラッシュ以降の部分を抽出します。これにより、http://www.w3.org/TR/html4/ から html4 のような、名前空間の内容を反映したプレフィックスを生成しようとします。
      • 無効なプレフィックスの代替: 抽出されたプレフィックスが空文字列、XML名として無効(isName 関数でチェック)、またはコロンを含む場合(プレフィックス自体が名前空間宣言と衝突する可能性があるため)、_ を代替プレフィックスとして使用します。
      • 予約語の回避: プレフィックスが xml で始まる場合、XMLの予約語と衝突する可能性があるため、_ を先頭に追加して _xml のように変更します。
      • 衝突解決: 生成しようとした prefixp.attrNS マップに既に存在する場合(つまり、別の名前空間に同じプレフィックスが割り当てられている場合)、prefix_1, prefix_2 のように連番を付加して一意なプレフィックスを生成します。p.seq は連番を管理するためのカウンタです。
      • マップへの登録: 最終的に決定した prefixurl のマッピングを p.attrPrefixp.attrNS に登録します。
      • xmlns: 属性の出力: 新しいプレフィックスが生成された場合、xmlns:prefix="url" 形式の属性をXML出力ストリームに書き込みます。
      • isNewtrue となります。
  • deleteAttrPrefix(prefix string) 関数: この関数は、createAttrPrefixisNewtrue となった場合に、defer p.deleteAttrPrefix(prefix) として呼び出されます。これにより、現在の要素のマーシャリングが完了し、スコープを抜ける際に、その要素内で定義された名前空間プレフィックスのマッピングを printer のマップから削除します。これは、名前空間のスコープが要素に限定されるというXMLのセマンティクスを正しく反映するために重要です。
  • marshalValue の変更: 以前は、名前空間を持つ属性に対して xmlns:_1="uri" のように機械的なプレフィックスを生成していましたが、この変更により p.createAttrPrefix(finfo.xmlns) を呼び出すようになりました。これにより、より人間が読みやすいプレフィックスが使用され、必要に応じて defer p.deleteAttrPrefix(prefix) でスコープが管理されます。

xml.go の変更点

  • const xmlURL = "http://www.w3.org/XML/1998/namespace": XMLの xml 名前空間のURIを定数として定義することで、コードの可読性と保守性が向上します。
  • translate 関数の変更: translate 関数は、XMLパーサーが要素名や属性名を処理する際に、名前空間の解決を行うためのものです。 case n.Space == "xml": n.Space = xmlURL の行が追加されました。これは、XMLドキュメント内で xml:lang のように xml プレフィックスが使用されている場合、その Space フィールドを対応するURI http://www.w3.org/XML/1998/namespace に変換することを意味します。これにより、パーサーが xml プレフィックスを正しく解釈し、その属性がXMLの組み込み名前空間に属することを認識できるようになります。

read_test.go の変更点

テストケースの追加と既存テストの期待値の更新は、これらの変更が正しく機能し、期待されるXML出力が生成されることを検証するために不可欠です。特に TestMarshalNSAttrwant 変数の変更は、新しいプレフィックス生成ロジック(例: _1 から html4furniturexml:lang の追加)が反映されていることを示しています。また、マーシャリング後のアンマーシャリングテストの追加は、ラウンドトリップ(Go構造体 → XML → Go構造体)が正しく行われることを保証します。

これらの変更は、Goの encoding/xml パッケージが生成するXMLの品質と標準への準拠を大幅に向上させるものです。

関連リンク

参考にした情報源リンク

参考にした情報源リンク