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

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

このコミットは、Go言語の標準ライブラリ encoding/xml パッケージに MarshalIndent 関数を追加し、XML出力の整形(インデント)を可能にするものです。また、既存のXMLマーシャリングの例を、より読みやすくするために整形された出力を使用するテストファイルに移動しています。これにより、生成されるXMLの可読性が向上し、デバッグや人間による確認が容易になります。

コミット

commit aed20a6951948ef7f6edd1f4160fc8c1d3e8df56
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date:   Thu Feb 16 02:01:46 2012 -0200

    encoding/xml: add MarshalIndent and move the example
    
    An unindented XML example is hard to follow. MarshalIndent
    allows moving the example over to a test file (and fixing it).
    
    R=golang-dev, r, gustavo, r, rsc
    CC=golang-dev
    https://golang.org/cl/5674050

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

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

元コミット内容

このコミットの目的は、Goの encoding/xml パッケージに新しい関数 MarshalIndent を導入することです。この関数は、XMLデータをバイトスライスにマーシャリングする際に、指定されたプレフィックスとインデント文字列を使用して整形された出力を生成します。これにより、生成されるXMLが視覚的に構造化され、人間にとって読みやすくなります。

また、コミットメッセージは、既存のXMLマーシャリングの例がインデントされていないために読みにくいという問題意識を表明しています。MarshalIndent の導入により、この例をテストファイル (example_test.go) に移動し、整形された出力で表示できるようになります。これにより、例の可読性が向上し、encoding/xml パッケージの利用者がXMLの構造をより簡単に理解できるようになります。

変更の背景

Go言語の encoding/xml パッケージは、Goの構造体とXMLデータの間で変換を行うための機能を提供します。これには、Goの構造体をXMLに変換する「マーシャリング」と、XMLをGoの構造体に変換する「アンマーシャリング」が含まれます。

従来の xml.Marshal 関数は、XMLデータを生成しますが、その出力はインデントされず、すべてが1行に連結された形式でした。これは機械処理には適していますが、人間がXMLの構造を理解したり、デバッグを行ったりする際には非常に読みにくいという問題がありました。特に、複雑なXML構造を持つデータの場合、インデントがないと要素の階層関係を視覚的に把握することが困難でした。

このコミットの背景には、以下の具体的なニーズがありました。

  1. 可読性の向上: 生成されるXMLの可読性を高め、開発者がXMLデータを容易に検査できるようにすること。これは、特にAPIのレスポンスや設定ファイルなど、人間が直接読み書きする可能性のあるXMLデータにおいて重要です。
  2. デバッグの容易化: XMLの構造が明確になることで、マーシャリングの際に発生する可能性のある問題を特定しやすくなる。
  3. 例の改善: encoding/xml パッケージのドキュメントや例において、整形されたXML出力を用いることで、利用者がXMLの構造とマーシャリングの挙動をより直感的に理解できるようにすること。コミットメッセージにあるように、「インデントされていないXMLの例は理解しにくい」という課題を解決するためです。

これらの背景から、MarshalIndent のような整形出力機能の追加が求められ、このコミットによって実現されました。

前提知識の解説

このコミットの変更内容を理解するためには、以下の前提知識が必要です。

1. XML (Extensible Markup Language)

XMLは、情報を構造化するためのマークアップ言語です。HTMLがウェブページの表示に特化しているのに対し、XMLはデータの記述と交換に特化しています。

  • 要素 (Elements): <tag>content</tag> のように、開始タグと終了タグで囲まれた構造。
  • 属性 (Attributes): 開始タグ内に name="value" の形式で記述される、要素に関する追加情報。
  • 階層構造: 要素は他の要素を内包することができ、ツリー状の階層構造を形成します。
  • 整形 (Pretty-printing): XMLデータを、インデントや改行を用いて視覚的に読みやすくする処理のこと。

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

Goの標準ライブラリである encoding/xml パッケージは、Goの構造体とXMLデータの間の変換(マーシャリングとアンマーシャリング)を提供します。

  • マーシャリング (Marshalling): Goの構造体のデータをXML形式のバイトスライスに変換するプロセス。xml.Marshal 関数がこれを行います。
  • アンマーシャリング (Unmarshalling): XML形式のバイトスライスをGoの構造体に変換するプロセス。xml.Unmarshal 関数がこれを行います。
  • 構造体タグ (Struct Tags): Goの構造体のフィールドに付与される文字列リテラルで、encoding/xml パッケージがXML要素名や属性名を決定するために使用します。例えば、xml:"name,attr" は、そのフィールドがXMLの name 属性に対応することを示します。
    • xml:"element_name": フィールドが対応するXML要素の名前を指定します。
    • xml:"element_name,attr": フィールドが対応するXML属性の名前を指定します。
    • xml:"parent>child": ネストされた要素のパスを指定します。
    • xml:",omitempty": フィールドがGoのゼロ値の場合、XML出力から省略されます。
    • xml:",comment": フィールドの内容がXMLコメントとして出力されます。
  • xml.Name: XML要素のローカル名と名前空間を表現する構造体です。構造体のフィールドに xml.Name 型の XMLName フィールドを埋め込むことで、ルート要素の名前を制御できます。

3. bufio.Writer

bufio.Writer は、Goの io パッケージの一部であり、バッファリングされたI/O操作を提供します。これにより、小さな書き込み操作が効率的にバッファリングされ、基になる io.Writer へのシステムコールが削減されます。encoding/xml パッケージの内部では、XMLデータを効率的に書き出すために bufio.Writer が使用されています。

4. reflect パッケージ

Goの reflect パッケージは、実行時に型情報を検査し、値を操作するための機能を提供します。encoding/xml パッケージは、Goの構造体のフィールドを動的に検査し、それらをXML要素や属性にマッピングするために reflect パッケージを広範に利用しています。

技術的詳細

このコミットの技術的詳細は、主に encoding/xml/marshal.gosrc/pkg/encoding/xml/example_test.go の変更に集約されます。

MarshalIndent 関数の追加

新しい関数 MarshalIndentmarshal.go に追加されました。

// MarshalIndent works like Marshal, but each XML element begins on a new
// indented line that starts with prefix and is followed by one or more
// copies of indent according to the nesting depth.
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
	var b bytes.Buffer
	enc := NewEncoder(&b)
	enc.prefix = prefix
	enc.indent = indent
	err := enc.marshalValue(reflect.ValueOf(v), nil)
	enc.Flush()
	if err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}
  • MarshalIndentMarshal と同様に interface{} 型の v を受け取りますが、加えて prefixindent という2つの文字列引数を取ります。
  • prefix は各行の先頭に付加される文字列(例: タブやスペース)です。
  • indent は各ネストレベルごとに繰り返されるインデント文字列です。
  • 内部的には bytes.Buffer を使用してXMLデータを構築し、NewEncoder で新しい Encoder インスタンスを作成します。
  • 作成された Encoderprefixindent フィールドに引数で渡された値を設定します。
  • enc.marshalValue を呼び出して実際のマーシャリング処理を行い、最後に enc.Flush() でバッファをフラッシュします。

Encoder および printer 構造体の変更

XMLのインデントを制御するために、Encoder が内部的に使用する printer 構造体に新しいフィールドが追加されました。

type printer struct {
	*bufio.Writer
	indent     string // 各ネストレベルのインデント文字列
	prefix     string // 各行の先頭に付加されるプレフィックス文字列
	depth      int    // 現在のネストの深さ
	indentedIn bool   // 直前の書き込みがインデントされた行の開始だったかを示すフラグ
}
  • indent: 各ネストレベルで繰り返されるインデント文字列(例: "\t"" ")。
  • prefix: 各行の先頭に一度だけ付加される文字列。
  • depth: 現在のXML要素のネストの深さを追跡します。
  • indentedIn: 直前の書き込みがインデントされた行の開始だったかどうかを示すフラグ。これにより、閉じタグの前に余分な改行とインデントが挿入されるのを防ぎます。

また、NewEncoder 関数も変更され、printer の初期化時に Writer フィールドを明示的に設定するようになりました。

--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -103,7 +96,7 @@ type Encoder struct {
 
 // NewEncoder returns a new encoder that writes to w.
 func NewEncoder(w io.Writer) *Encoder {
-	return &Encoder{printer{bufio.NewWriter(w)}}
+	return &Encoder{printer{Writer: bufio.NewWriter(w)}}
 }

writeIndent 関数の追加

printer 構造体に新しいメソッド writeIndent が追加されました。この関数が実際のインデント処理を行います。

func (p *printer) writeIndent(depthDelta int) {
	if len(p.prefix) == 0 && len(p.indent) == 0 {
		return // インデントが不要な場合は何もしない
	}
	if depthDelta < 0 { // 閉じタグの場合
		p.depth-- // ネストレベルを減らす
		if p.indentedIn {
			p.indentedIn = false
			return // 直前がインデントされた行の開始だった場合、余分な改行・インデントは不要
		}
		p.indentedIn = false
	}
	p.WriteByte('\n') // 改行を書き込む
	if len(p.prefix) > 0 {
		p.WriteString(p.prefix) // プレフィックスを書き込む
	}
	if len(p.indent) > 0 {
		for i := 0; i < p.depth; i++ {
			p.WriteString(p.indent) // インデント文字列をネストレベルの回数だけ書き込む
		}
	}
	if depthDelta > 0 { // 開始タグの場合
		p.depth++ // ネストレベルを増やす
		p.indentedIn = true // インデントされた行の開始フラグを立てる
	}
}
  • depthDelta は、インデントを書き込む際にネストの深さをどのように調整するかを示します(+1 は開始タグ、-1 は閉じタグ、0 はコメントなど)。
  • prefixindent が設定されていない場合は、インデント処理はスキップされます。
  • 常に改行を書き込みます。
  • prefix が設定されていれば、それを書き込みます。
  • indent が設定されていれば、現在の depth の回数だけ indent 文字列を繰り返して書き込みます。
  • depthDelta に応じて depth を増減させ、indentedIn フラグを適切に設定します。

marshalValue および parentStack の変更

marshalValue 関数と parentStack 構造体(XML要素の階層を管理)の内部で、writeIndent が適切に呼び出されるように変更されました。

  • 開始タグの書き込み前: p.writeIndent(1) が呼び出され、新しい要素の開始前に改行とインデントが挿入され、depth がインクリメントされます。
  • 閉じタグの書き込み前: p.writeIndent(-1) が呼び出され、要素の閉じタグの前に改行とインデントが挿入され、depth がデクリメントされます。
  • コメントの書き込み前: p.writeIndent(0) が呼び出され、コメントの前に改行とインデントが挿入されます。
  • parentStacktrim および push メソッドでも writeIndent が使用されるようになり、ネストされた要素の開始・終了時にも適切なインデントが適用されます。

example_test.go の追加と例の移動

src/pkg/encoding/xml/example_test.go という新しいファイルが追加され、ExampleMarshalIndent という関数が定義されました。

func ExampleMarshalIndent() {
	type Person struct {
		XMLName   xml.Name `xml:"person"`
		Id        int      `xml:"id,attr"`
		FirstName string   `xml:"name>first"`
		LastName  string   `xml:"name>last"`
		Age       int      `xml:"age"`
		Height    float32  `xml:"height,omitempty"`
		Married   bool
		Comment   string `xml:",comment"`
	}

	v := &Person{Id: 13, FirstName: "John", LastName: "Doe", Age: 42}
	v.Comment = " Need more fields. "

	output, err := xml.MarshalIndent(v, "\t", "\t")
	if err != nil {
		fmt.Printf("error: %v\n", err)
	}

	os.Stdout.Write(output)
}

この例では、Person 構造体を定義し、MarshalIndent を使用してXMLにマーシャリングしています。prefixindent にはそれぞれタブ文字 ("\t") が指定されており、これにより整形されたXMLが出力されます。このテストは、go test コマンドで実行される際に、出力が期待されるXMLと一致するかどうかを検証します。

また、marshal.go 内にあった古いコメント形式のXML例は削除され、MarshalIndent の例を参照するように変更されました。

--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -60,32 +60,9 @@ const (
 //
 // If a field uses a tag "a>b>c", then the element c will be nested inside
 // parent elements a and b.  Fields that appear next to each other that name
-// the same parent will be enclosed in one XML element.  For example:
+// the same parent will be enclosed in one XML element.
 //
-//	type Result struct {
-//		XMLName   xml.Name `xml:"result"`
-//		Id        int      `xml:"id,attr"`
-//		FirstName string   `xml:"person>name>first"`
-//		LastName  string   `xml:"person>name>last"`
-//		Age       int      `xml:"person>age"`
-//		Height    float    `xml:"person>height,omitempty"`
-//		Married   bool     `xml:"person>person>married"`
-//	}\n-//
-//	xml.Marshal(&Result{Id: 13, FirstName: "John", LastName: "Doe", Age: 42})\n-//
-// would be marshalled as:\n-//
-//	<result>\n-//		<person id="13">\n-//			<name>\n-//				<first>John</first>\n-//				<last>Doe</last>\n-//			</name>\n-//			<age>42</age>\n-//			<married>false</married>\n-//		</person>\n-//	</result>
+// See MarshalIndent for an example.
 //
 // Marshal will return an error if asked to marshal a channel, function, or map.
 func Marshal(v interface{}) ([]byte, error) {

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

このコミットにおけるコアとなるコードの変更箇所は以下の通りです。

  1. src/pkg/encoding/xml/marshal.go:

    • MarshalIndent 関数の新規追加 (L96-L110)
    • printer 構造体への indent, prefix, depth, indentedIn フィールドの追加 (L118-L121)
    • NewEncoder 関数での printer 初期化の変更 (L114)
    • writeIndent 関数の新規追加 (L354-L377)
    • marshalValue 関数内での p.writeIndent(1) (L177) および p.writeIndent(-1) (L216) の追加
    • marshalStruct 関数内でのコメント書き込み時の p.writeIndent(0) の追加 (L294)
    • parentStack 構造体の trim メソッド内での s.writeIndent(-1) の追加 (L379)
    • parentStack 構造体の push メソッド内での s.writeIndent(1) および s.s.WriteByte('<') の変更 (L388-L389)
    • Marshal 関数のコメントからXML例の削除と MarshalIndent への参照追加 (L60-L86)
  2. src/pkg/encoding/xml/example_test.go:

    • 新しいテストファイルとして追加され、ExampleMarshalIndent 関数が定義されています。このファイルは、MarshalIndent の使用例と、その出力がどのように整形されるかを示しています。

コアとなるコードの解説

MarshalIndent 関数

この関数は、encoding/xml パッケージの新しい公開APIであり、XMLデータの整形出力機能を提供します。

  • v interface{}: マーシャリングするGoの構造体やデータを受け取ります。
  • prefix string: 生成されるXMLの各行の先頭に付加される文字列です。例えば、XML宣言の前に特定の文字列を挿入したい場合などに使用できます。
  • indent string: 各ネストレベルごとに繰り返されるインデント文字列です。通常はタブ ("\t") やスペース (" ") が指定されます。

内部的には、Encoder を作成し、その prefixindent フィールドを設定することで、後続のマーシャリング処理が整形された出力を生成するように構成します。

printer 構造体の拡張

printer 構造体は、XMLデータを実際に書き出すための内部ヘルパーです。このコミットで追加されたフィールドは、インデント処理の状態を管理するために不可欠です。

  • indent, prefix: MarshalIndent に渡されたインデントとプレフィックスの文字列を保持します。
  • depth: 現在書き込んでいるXML要素のネストの深さを追跡します。ルート要素の深さは0から始まり、子要素が追加されるたびに増加します。
  • indentedIn: 直前の書き込みがインデントされた行の開始だったかどうかを示すフラグです。これにより、閉じタグの前に余分な改行とインデントが挿入されるのを防ぎ、XMLの整形をより自然に見せます。

writeIndent 関数

この関数は、XMLの各要素の開始タグ、終了タグ、コメントの前に適切な改行とインデントを挿入する役割を担います。

  • depthDelta int: この引数は、writeIndent が呼び出された時点でのネストの深さの変更を示します。
    • +1: 新しい要素の開始タグを書き込む前。depth をインクリメントし、indentedIntrue に設定します。
    • -1: 要素の閉じタグを書き込む前。depth をデクリメントし、indentedInfalse に設定します。
    • 0: コメントなど、ネストの深さが変わらない場合。
  • この関数は、まず改行を書き込み、次に prefix を書き込み、最後に現在の depth に応じて indent 文字列を繰り返して書き込みます。これにより、XMLの階層構造が視覚的に表現されます。

marshalValue および parentStack の変更

これらの関数は、XML要素の開始タグ、終了タグ、およびネストされた要素のパスを処理する際に、writeIndent を呼び出すように変更されました。これにより、XMLの各部分が書き出されるたびに、適切なインデントが適用されるようになります。特に、parentStack はXMLの階層を管理し、親要素の開始・終了タグを適切に書き出す役割を担っているため、ここでの writeIndent の適用は、複雑なネスト構造を持つXMLの整形に不可欠です。

example_test.go の追加

この新しいテストファイルは、MarshalIndent の具体的な使用例を提供します。Goの testing パッケージの Example 関数は、ドキュメントの一部として自動的に生成され、実行可能な例として機能します。これにより、開発者は MarshalIndent の使い方と、それが生成するXMLの形式を簡単に確認できます。また、この例をテストファイルに移動することで、コードベースの変更によって例が壊れていないかを継続的に検証できるようになります。

これらの変更により、encoding/xml パッケージは、人間が読みやすい整形されたXML出力を生成する機能を手に入れ、パッケージの使いやすさとデバッグの容易性が大幅に向上しました。

関連リンク

参考にした情報源リンク