Go言語でUTF-8ではないXMLデータを構造体にマッピングする

一度できればなんてことはないのですが、少しハマったので残しておきます。

この記事でやること

先に断りを入れると、下記の記事とStack Overflowを大いに参考にさせていただきました。
違いはHTTPリクエストであることと、文字コードEUC-JPであることだけです。

devlights.hatenablog.com

stackoverflow.com

前提条件

対象のAPIによって取得されるXML

<?xml version="1.0" encoding="EUC-JP"?>
<data>
  <status>OK</status>
  <person>
    <familyname>山田</familyname>
    <givenname>太郎</givenname>
    <age>20</age>
    <birthday format="yyyy-MM-dd">1999-01-01</birthday>
  </person>
  <person>
    <familyname>佐藤</familyname>
    <givenname>治郎</givenname>
    <age>20</age>
    <birthday format="yyyy-MM-dd">1999-01-01</birthday>
  </person>
</data>

構造体の定義

package person

import "encoding/xml"

type Data struct {
    XMLName xml.Name `xml:"data"` // ルート要素。なくてもOK
    Status  string   `xml:"status"`
    Person  []Person `xml:"person"`
}

type Birthday struct {
    Format string `xml:"format,attr"`
    Date   string `xml:",chardata"`
}
type Person struct {
    FamilyName string   `xml:"familyname"`
    GivenName  string   `xml:"givenname"`
    Age        int      `xml:"age"`
    Birthday   Birthday `xml:"birthday"`
}

本処理

package person

import (
    "bytes"
    "context"
    "encoding/xml"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
   
    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

func main() {
    ctx := context.Background()
    response, err := getXML[Data](ctx)
    if err != nil {
        fmt.Println(err)
    }
    // jsonに変換して出力
    res, err := json.Marshal(response)
    fmt.Println(string(res))
}

func getXML[T any](ctx context.Context) (data T, err error) {
    url := "https://example.com/api/persons"

    // HTTP clientを作成
    httpClient := &http.Client{
        // 省略
    }

    // HTTP Requestを作成
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return data, fmt.Errorf("create request failed: %s", err)
        }
    // HTTP Requestを実行
    resp, err := httpClient.Do(req)
    if err != nil {
        return data, fmt.Errorf("http request failed: %s", err)
    }
    defer resp.Body.Close()
   
    // HTTP Response
    respBytes, err := io.ReadAll(resp.Body)
    if err != nil {
        return data, fmt.Errorf("read response failed: %s", err)
    }
   
    // XMLのデコード(ポイント1)
    decoder := xml.NewDecoder(bytes.NewReader(respBytes))
    decoder.CharsetReader = makeCharsetReader // ポイント2
    if err = decoder.Decode(&data); err != nil {
        return data, fmt.Errorf("XML parse failed: %s", err)
    }
    return data, nil
}

func makeCharsetReader(charset string, input io.Reader) (io.Reader, error) {
    if charset == "EUC-JP" {
        return transform.NewReader(input, japanese.EUCJP.NewDecoder()), nil
    }
    return input, nil
}

解説

ポイント1

XMLパッケージにはUnmarshal関数がありますが、今回はHTTPリクエストで取得したデータをデコードしたいので、Decodeを使います。
Unmarshal関数を利用した場合、下記のエラーが発生します。

xml: encoding \"EUC-JP\" declared but Decoder.CharsetReader is nil"

ポイント2

Decodeを実行する際に、EUC-JPで読み出せるio.Readerを渡す必要があります。
ここでは記事を参考に、EUC-JPの場合はtransformパッケージを利用してラップしたものを返却し、それ以外はそのまま返却します。

参考情報