↔️

【Go】XMLデータのさまざまなエンコーディング方法

に公開

はじめに

現在携わっているプロジェクトで、XMLを扱った処理を行うところがありました。
今までJSON形式しか扱ったことがなく、XMLデータを扱うにあたって調べたことを備忘録的に書いていこうと思います。

この記事でわかること

  • GoでのXMLデータの基本的な扱い方
  • ネストやスライスデータの扱い方
  • タグを使ったさまざまな制御の仕方

基本

GoでXMLデータを扱うには、encoding/xmlパッケージを使います。
基本的な使い方は以下の通りです。

main.go
type User struct {
	XMLName xml.Name `xml:"user"` // ルート要素を指定
	ID      int      `xml:"id"`   // タグ名を指定
	Name    string   `xml:"name"`
	Email   string   `xml:"email"`
}

func main() {
    // ルート名は構造体で定義済みなので指定する必要はない
    user := User{ID: 1, Name: "Tomoya", Email: "dummy@example.com"}
    // 改行とインデントの整形を行った上でエンコーディングするメソッド
    output, err := xml.MarshalIndent(user, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    // バイトスライスなので文字列に変換
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<user>
 <id>1</id>
 <name>Tomoya</name>
 <email>dummy@example.com</email>
</user>

構造体のフィールドでルート要素とその中のデータを定義し、フィールドタグ(xml:"要素名")を付与することで、xml.MarshalIndent()を使って構造体からXMLへ階層的にエンコードできます。

いろいろな使い方

前述の通り、構造体フィールドにxml:"要素名"と指定するのが基本的な使い方です。
しかし、XMLではタグの指定だけでなく、ネストされた構造体や配列(スライス) を扱うこともできます。
ここでは、タグの指定方法に加えて、入れ子構造や複数要素を持つデータの扱い方も一つずつ見ていきましょう。

ネストする

今回は、ユーザーデータを扱う構造体の中に、住所を扱う構造体が入れ子構造的になっているケースを見ていきましょう。例えば、以下のように記述することができます。

main.go
type User struct {
    XMLName xml.Name `xml:"user"`
    ID      int      `xml:"id"`
    Name    string   `xml:"name"`
    Email   string   `xml:"email"`
    Address Address  `xml:"address"` // 入れ子構造になっている
}

type Address struct {
    State string `xml:"state"`
    City  string `xml:"city"`
}

func main() {
    user := User{
        ID:    1,
        Name:  "Tomoya",
        Email: "dummy@example.com",
        Address: Address{
            State: "Japan",
            City:  "Tokyo",
        },
    }
    output, err := xml.MarshalIndent(user, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<user>
 <id>1</id>
 <name>Tomoya</name>
 <email>dummy@example.com</email>
 <address>
  <state>Japan</state>
  <city>Tokyo</city>
 </address>
</user>

スライス(繰り返し)

今回は、複数人のユーザーデータを扱うケースを見ていきましょう。例えば、以下のように記述することができます。

main.go
type Users struct {
    XMLName xml.Name `xml:"users"`
    User    []User   `xml:"user"`
}

type User struct {
	ID      int      `xml:"id"`
	Name    string   `xml:"name"`
	Email   string   `xml:"email"`
}

func main() {
    users := Users{
        User: []User{
          {ID: 1, Name: "Tomoya", Email: "dummy@example.com"}, 
          {ID: 2, Name: "Taro", Email: "taro@example.com"},
        },
    }
    output, err := xml.MarshalIndent(users, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<users>
 <user>
  <id>1</id>
  <name>Tomoya</name>
  <email>dummy@example.com</email>
 </user>
 <user>
  <id>2</id>
  <name>Taro</name>
  <email>taro@example.com</email>
 </user>
</users>

属性を使う

ここからは、タグを使ったデータの扱い方を見ていきましょう。
まずは属性を指定する方法です。構造体フィールドに,attrタグを付与することで、そのフィールドは要素の属性となります。

main.go
type User struct {
    XMLName xml.Name `xml:"user"`
    ID      int      `xml:"id,attr"` // 属性を指定
    Name    string   `xml:"name"`
    Email   string   `xml:"email"`
}

func main() {
    user := User{ID: 1, Name: "Tomoya", Email: "dummy@example.com"}
    output, err := xml.MarshalIndent(user, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<user id="1">
 <name>Tomoya</name>
 <email>dummy@example.com</email>
</user>

上記の場合、userというルート要素を識別するidという属性を追加することができました。
複数のデータを扱う際に効力を発揮します。

要素の中のテキストとして出力

今度は、フィールドの値をタグを使って表現するのではなく、テキストとして扱う方法を見ていきます。構造体フィールドに,chardataタグを付けることで、そのフィールドをテキストして扱うことができます。

main.go
type Message struct {
    XMLName xml.Name `xml:"message"`
    Text    string   `xml:",chardata"` // テキストデータのみ扱うよう指定
}

func main() {
    message := Message{Text: "Example Message"}
    output, err := xml.MarshalIndent(message, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<message>Example Message</message>

上記の場合、messageというルート要素の中に、Example Messageというテキストのみ出力しています。

コメント

今度は、フィールドをコメントとして扱う方法を見ていきます。構造体フィールドに,commentタグを付けると、そのフィールド値をコメント()として出力できます。

main.go
type Message struct {
    XMLName xml.Name `xml:"message"`
    Text    string   `xml:"text"`
    Comment string   `xml:",comment"` // コメントとして扱うよう指定
}

func main() {
    message := Message{Text: "Example Message", Comment: "Example Comment"}
    output, err := xml.MarshalIndent(message, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<message>
 <text>Example Message</text>
 <!--Example Comment-->
</message>

上記のように、Example Commentという部分がコメントされていることがわかります。

データ出力を制御

今度は、フィールドの値を出力するか否かを制御するタグを見ていきます。
JSONデータを扱うとき同様、omitempty-を指定することができます。
omitemptyは値がゼロ値(空文字や0など)の場合にその要素を出力しないようにし、-はそのフィールド自体を完全に無視します。

main.go
type Example struct {
    XMLName xml.Name `xml:"example"`
    ID      int      `xml:"id"`
    Name    string   `xml:"name,omitempty"`
    Ignored string   `xml:"-"`
}

func main() {
    // Nameに値がない場合を想定
    example := Example{ID: 1, Name: "",  Ignored: "Ignored"}
    output, err := xml.MarshalIndent(example, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<example>
 <id>1</id>
</example>

上記のように、Nameは値がゼロ値のため出力されず、Ignoredは無視するように制御しているため出力されていません。

埋め込み

今度は、XMLの中身をそのまま文字列として取得したい、または埋め込みたいときに使うタグを見ていきます。innerxmlタグを指定することで実現することができます。
今回は、複数の記事データがあり、本文に<p>タグなどのXMLをそのまま保持したいケースを見ていきましょう。

main.go
type Articles struct {
    XMLName  xml.Name  `xml:"articles"`
    Article []Article `xml:"article"`
}

type Article struct {
    ID      int     `xml:"id,attr"`
    Title   string  `xml:"title"`
    Content Content `xml:"content"`
}

type Content struct {
    Body string `xml:",innerxml"`
}

func main() {
    articles := Articles{
        Article: []Article{
            {
                ID:    1,
                Title: "タイトル1",
                Content: Content{
                    Body: "<p>本文1</p>",
                },
            },
            {
                ID:    2,
                Title: "タイトル2",
                Content: Content{
                    Body: "<p>本文2</p>",
                },
            },
        },
    }
    output, err := xml.MarshalIndent(articles, "", " ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(output))
}

実行すると、以下のように出力されます。

ターミナル
# go run main.go

<articles>
 <article id="1">
  <title>タイトル1</title>
  <content><p>本文1</p></content>
 </article>
 <article id="2">
  <title>タイトル2</title>
  <content><p>本文2</p></content>
 </article>
</articles>

まとめ

今回は、GoにおけるXMLデータの扱い方を見ていきました。
ネストされたデータやスライスデータはJSONデータの時と似たような扱い方をする一方で、タグを用いた制御ではXML独自のやり方があるなと感じました。
今回はエンコードについての記事を書いていきましたが、どこかのタイミングでデコードに関する記事も書きたいなと思います!

参考

Discussion