概要
こんにちは、@ono_matope です。今年はGoでencoding/xml
をよく使ったので、このパッケージの少しだけ突っ込んだ使い方を解説をしてみようと思います。
ここでは、encoding/xml
の基本的な使い方から、動的に変化するXMLの扱い方までを説明します。
基本的な使い方
まずは基本的な使い方です。encoding/xml
を使うと、Goの構造体をXML文書に変換(Marshal)、またはその逆(Unmarshal)が出来ます。例えばこんな構造体PersonをXMLにMarshal/Unmarshalが簡単に出来ます。
例1: 標準のMarshalとUnmarshal
Demo: http://play.golang.org/p/bjCr7Fpk_8
package main
import (
"encoding/xml"
"fmt"
)
type Name struct {
First, Last string
}
type Person struct {
Name Name
Gender string
Age int
}
func main() {
// Marshalの例
john := Person{Name{"John", "Doe"}, "Male", 20}
buf, _ := xml.MarshalIndent(john, "", " ")
fmt.Println(string(buf))
// Unmarshalの例
xmldoc := []byte(`
<Person>
<Name><First>John</First><Last>Doe</Last></Name>
<Gender>Male</Gender>
<Age>20</Age>
</Person>`)
p := Person{}
xml.Unmarshal(xmldoc, &p)
fmt.Println(p)
}
出力
<Person>
<Name>
<First>John</First>
<Last>Doe</Last>
</Name>
<Gender>Male</Gender>
<Age>20</Age>
</Person>
{{John Doe} Male 20}
構造体フィールドタグ
簡単ですね。しかし、XML要素の名前が構造体メンバの名前にしまう、XML属性が出力できない、などいろいろ不便です。構造体タグを使う事で、以下のような事が出来るようになります
- 構造体メンバとXML要素を別の名前にする
- 構造体メンバをXML要素ではなくXML属性に対応させる
- 構造体メンバをXMLに出力させない
- 構造体メンバをXMLコメントに対応させる
- などなど…
構造体フィールドタグの詳細な仕様については xml - The Go Programming Language を参照してください。
例2: さまざまな構造体フィールドタグ
Demo: http://play.golang.org/p/31lOxJLafK
package main
import "fmt"
import "encoding/xml"
func main() {
type Person struct {
XMLName xml.Name `xml:"person"` // Person2型のXML要素名をpersonにする
Name string
Gender string `xml:"attr"` // XML属性にする
Age int `xml:"age"` // XMl要素名をageにする
ID int `xml:"-"` // XML要素にしない
Note string `xml:",omitempty"` // 空のときXML要素をつくらない
Comment string `xml:",comment"` // XMLコメントにする
}
// Marshalの例
john := Person{Name: "John Doe", Gender: "Male", Age: 20, ID: 1, Note: "", Comment: "ノーコメント"}
buf, _ := xml.MarshalIndent(john, "", " ")
fmt.Println(string(buf))
// Unmarshalの例
var p Person
xml.Unmarshal(buf, &p)
fmt.Printf("%+v", p)
}
出力
<person>
<Name>John Doe</Name>
<attr>Male</attr>
<age>20</age>
<!--ノーコメント-->
</person>
{XMLName:{Space: Local:person} Name:John Doe Gender:Male Age:20 ID:0 Note: Comment:ノーコメント}
xml.Marshaler と xml.Unmarshaler interface
しかし、構造体タグを使っても、XMLに出力したい値は全て構造体のメンバ値として保持している必要がありました。これは、例えば以下のような、変数とXMLの構造に違いがあるケースで問題になります。
- 構造体はtime.Time型で誕生日を持っているが、XMLには生年のみ出力したい
- xmlns属性のようなロジックと関係ない要素や属性をXMLに出力したい
- 構造体は小文字のメンバを持っているが、XMLに出力したい(通常小文字のメンバは無視されます)
これらの要件を実装するにはどうしたらよいでしょうか?XML出力用の、BirthYearやxmlnsメンバを持つ構造体(UserInfoForXML)を定義して、元の構造体からコピーしてMarshalする?大変ですね。
そんな時は、構造体にxml.Marshaler
, xml.Unmarshaler
インターフェイスを実装することで、そのような構造体とXMLの変換ロジックを MarshalXML
/ UnmarshalXML
メソッド内でスマートに記述する事が出来ます。
// xml.Marshalerインターフェイスの定義
type Marshaler interface {
MarshalXML(e *Encoder, start StartElement) error
}
// xml.Unmarshalerインターフェイスの定義
type Unmarshaler interface {
UnmarshalXML(d *Decoder, start StartElement) error
}
xml - The Go Programming Language
例3: 独自のMarshalXML, UnmarshalXMLメソッドを定義する
Demo: http://play.golang.org/p/8PygrpakPd
具体的には、このように実装します。この例では、以下のフォーマットのXMLと構造体を相互変換します。両者には以下の違いがあります
- XMLにはxmlns要素がある
- 構造体の小文字要素をXMLに出力する
- 構造体で生年月日であるBirthTimeのうち、生年のみをXMLに出力する
type User struct {
Name string
gender string
BirthTime time.Time
}
<User xmlns="http://www.w3.org/2001/XMLSchema-instance">
<Name>Alice</Name>
<Gender>Female</Gender>
<BirthYear>2002</BirthYear>
</User>
package main
import (
"encoding/xml"
"errors"
"fmt"
"time"
)
type User struct {
Name string
gender string
BirthTime time.Time
}
func (u User) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
// xml.Encoder に食わせる身代わり構造体の定義と初期化
uu := struct {
Xmlns string `xml:"xmlns,attr"`
Name, Gender string
BirthYear int
}{
"http://www.w3.org/2001/XMLSchema-instance",
u.Name,
u.gender,
u.BirthTime.Year(),
}
return e.EncodeElement(uu, start)
}
func (u *User) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// xml.Decoder に食わせる身代わり構造体の定義と初期化
uu := struct {
Xmlns string `xml:"xmlns,attr"`
Name, Gender string
BirthYear int
}{}
if err := d.DecodeElement(&uu, &start); err != nil {
return err
}
// 入力値のバリデーションも可能。エラーを投げるとUnmarshal処理が停止する
if uu.Gender != "Male" && uu.Gender != "Female" {
return errors.New("Invalid Gender parameter")
}
*u = User{uu.Name, uu.Gender, time.Date(uu.BirthYear, time.January, 1, 0, 0, 0, 0, time.UTC)}
return nil
}
func main() {
u := User{"Alice", "Female", time.Date(2002, time.November, 10, 23, 0, 0, 0, time.UTC)}
buf, _ := xml.MarshalIndent(u, "", " ")
fmt.Println(string(buf))
u2 := User{}
xml.Unmarshal(buf, &u2)
fmt.Printf("%+v\n", u2)
}
出力
<User xmlns="http://www.w3.org/2001/XMLSchema-instance">
<Name>Alice</Name>
<Gender>Female</Gender>
<BirthYear>2002</BirthYear>
</User>
{Name:Alice gender:Female BirthTime:2002-01-01 00:00:00 +0000 UTC}
解説
xmlnsの使い方がおかしいですが、いい例題が浮かびませんでした…。MarshalXML
のレシーバは値、UnmarshalXML
のレシーバはポインタである事に注意してください。
MarshalXML
では、メソッド内でXMLレイアウトに対応した匿名構造体を定義し、レシーバであるUser型の値を使って初期化したものをxml.Encoder.EncodeElement()
でXMLにエンコードしています。
一方 UnmarshalXML
では、メソッド内でXMLレイアウトに対応した匿名構造体を定義し、xml.Decoder.DecodeElement()
でデコードした匿名構造体から、レシーバであるUser型に値を代入しています。
おまけ:動的なXMLを扱う
最後に、動的に変動するXML文書をMarshal/Unmarshalする方法を紹介します。たとえば、mapのキーバリューをXML要素として出力する事が出来ます。
例4 mapのキーバリューをXML要素にマッピングする
Demo: http://play.golang.org/p/G89t4CAAWz
この例では、文字列IDとmapのメンバを持つ構造体と、それをマッピングしたXMLの変換を扱います。IDはXML属性として扱われます。
type UserInfo struct {
ID string
Extra map[string]string
}
<User id="0001">
<Name>Charley</Name>
<Gender>Male</Gender>
<County>Japan</County>
<BloodType>A</BloodType>
<Hobby>Internet</Hobby>
</User>
package main
import (
"encoding/xml"
"fmt"
)
type UserInfo struct {
ID string
Extra map[string]string
}
func (u UserInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
start.Name.Local = "User"
start.Attr = []xml.Attr{{Name: xml.Name{Local: "id"}, Value: u.ID}}
e.EncodeToken(start)
for k, v := range u.Extra {
e.EncodeElement(v, xml.StartElement{Name: xml.Name{Local: k}})
}
e.EncodeToken(start.End())
return nil
}
func (u *UserInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
u.Extra = make(map[string]string)
// startタグの属性を走査
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
u.ID = attr.Value
}
}
// Decoder.Token()で子Tokenの走査
for {
token, err := d.Token()
if token == nil {
break
}
if err != nil {
return err
}
if t, ok := token.(xml.StartElement); ok {
var data string
if err := d.DecodeElement(&data, &t); err != nil {
return err
}
u.Extra[t.Name.Local] = data
}
}
return nil
}
func main() {
u := UserInfo{
ID: "0001",
Extra: map[string]string{
"Name": "Charley",
"Gender": "Male",
"County": "Japan",
"BloodType": "A",
"Hobby": "Internet",
},
}
buf, _ := xml.MarshalIndent(u, "", " ")
fmt.Println(string(buf))
u2 := UserInfo{}
if err := xml.Unmarshal(buf, &u2); err != nil {
fmt.Println(err.Error())
}
fmt.Printf("%#v\n", u2)
}
出力
<User id="0001">
<Name>Charley</Name>
<Gender>Male</Gender>
<County>Japan</County>
<BloodType>A</BloodType>
<Hobby>Internet</Hobby>
</User>
main.UserInfo{ID:"0001", Extra:map[string]string{"Name":"Charley", "Gender":"Male", "County":"Japan", "BloodType":"A", "Hobby":"Internet"}}
解説
MarshalXML
マーシャル処理は、start
としてMarshalXMLメソッドにStartElementが開きタグのトークンが渡ってくるので、
-
e.EncodeToken(start)
で開きタグのトークンをエンコード -
e.EncodeElement(v, xml.StartElement{Name: xml.Name{Local: k}})
でXML要素(開きタグ-コンテンツ-閉じタグがセットになったもの)をエンコード -
e.EncodeToken(start.End()
で閉じタグのトークンをエンコード
という流れになります。
UnmarshalXML
アンマーシャル処理は、Decoder.Token()
メソッドで一つずつトークンを取り出していき、開きタグ(StartElement)であればDecoder.DecodeElement(&data, &t)
として値を取り出していく流れになります。(ここではフラット以上の構造を持つXMLについては扱いません)
まとめ
いかがだったでしょうか。標準のencoding/xmlでもなかなか表現力がある事が分かって頂けたかと思います。Goで変なXMLを扱う時に助けになれば幸いです。