GoでJSONの一部分を利用者が定義した構造体に読み込める便利な手法を見つけた

## はじめに
GoでJSONの一部分を利用者が定義した構造体に読み込めるようなAPIの作り方の紹介です。Zabbix APIのクライアントを書いてみて気付きました。

ただ、なんとなく思いついて試してみたら出来たという話で将来にわたって安心して使える手法なのかは不明です。その点はご了承ください。

## その手法とは

Zabbixドキュメントの[APIのページ](https://www.zabbix.com/documentation/2.4/manual/api)を見ると、レスポンスのJSONは

```json:
{
    "jsonrpc": "2.0",
    "result": [
        {
            "hostid": "10084",
            "host": "Zabbix server",
            "interfaces": [
                {
                    "interfaceid": "1",
                    "ip": "127.0.0.1"
                }
            ]
        }
    ],
    "id": 2
}
```

とか

```json:
{
    "jsonrpc": "2.0",
    "result": {
        "itemids": [
            "24759"
        ]
    },
    "id": 3
}
```

といった感じになります。 `result` 値の構造がAPIによって違うわけです。

これに対して今回の手法をデモする最小限のコードは以下のような感じです。
https://play.golang.org/p/rmp6ipFc17

```go:
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type response struct {
	Jsonrpc string      `json:"jsonrpc"`
	Result  interface{} `json:"result,string"`
	ID      uint64      `json:"id"`
}

func someFunc(result interface{}) error {
	resBytes := []byte(`{"jsonrpc":"2.0","result":{"itemids":["24759"]},"id":3}`)
	res := response{Result: result}
	return json.Unmarshal(resBytes, &res)
}

func main() {
	var result struct {
		ItemIDs []string `json:"itemids"`
	}
	err := someFunc(&result)
	if err != nil {
		log.Fatal(err)
	}
	for i, itemID := range result.ItemIDs {
		fmt.Printf("itemid[%d]=%s\n", i, itemID)
	}
}
```

`response` 構造体の `Result` フィールドの型を `interface{}` にしているのがポイントです。

`someFunc()` がライブラリが提供するAPIという想定ですが、 `result` を戻り値ではなく `interface{}` 型の  引数としています。

`someFunc()` の中では受け取った `result` を `response` 型のローカル変数 `res` の `Result` フィールドにセットしてから `json.Unmarshal()` で全体の JSON をデコードしています。

APIの利用側は `main()` でやっているように、受け取りたいフィールドを持つ構造体の変数を用意して、そのアドレスを渡してAPIを呼び出します。ここでは [10 things you (probably) don't know about Go](https://talks.golang.org/2012/10things.slide#1) で紹介されている Anonymous structs を使っていますが、 `type` で構造体を定義してから使うことももちろん可能です。

この手法に気づく前は、 `Result` フィールドの型を [json.RawMessage](https://golang.org/pkg/encoding/json/#RawMessage)にしておいて、後で別途バイト配列からデコードするというやり方を考えていました。ですが、この手法ならば全体で一回デコードするだけですみます。

## この手法での実装例

この手法を用いて実装してみたZabbix APIのクライアントを[hnakamur/go-zabbix: A minimal Zabbix API client for Go](https://github.com/hnakamur/go-zabbix)に置いています。利用例は[go-zabbix/cmd/example/main.go](https://github.com/hnakamur/go-zabbix/blob/master/cmd/example/main.go)を参照してください。

APIドキュメントは[zabbix - GoDoc](https://godoc.org/github.com/hnakamur/go-zabbix)です。まだ全然書いてないですが。

### メソッドの説明

#### NewClient
[NewClient](https://godoc.org/github.com/hnakamur/go-zabbix#NewClient)でクライアントを生成します。第1引数 `zabbixURL` はZabbixのURLです。

第2引数 `zabbixHost` に空文字以外を設定するとリクエストの `Host` ヘッダに設定します ([GoでhttpリクエストにHostを設定するにはreq.Headerではなくreq.Hostを使う - Qiita](http://qiita.com/hnakamur/items/8a8ad0813eb577fc9018) 参照)。
URLは `http://127.0.0.1/` のようにIPアドレスで指定したいが、ウェブサーバではバーチャルホストで振り分けている場合に使います。

第3引数 `logger` は [Logger](https://godoc.org/github.com/hnakamur/go-zabbix#Logger) インタフェースの実装を渡すと、リクエストとレスポンスのJSONをログ出力します。 `nil` にするとログ出力しません。

#### Login
[Client.Login](https://godoc.org/github.com/hnakamur/go-zabbix#Client.Login)が[user.login ](https://www.zabbix.com/documentation/2.4/manual/api/reference/user/login) API用のメソッドです。 [Authentication](https://www.zabbix.com/documentation/2.4/manual/api#authentication)に説明されていますが、ログインのレスポンスで返された値を、以降のAPI呼び出しのリクエストの `auth` フィールドに設定する必要があるため、専用のメソッドとしています。

### CallとCallForCount

それ以外のAPIは[Client.Call](https://godoc.org/github.com/hnakamur/go-zabbix#Client.Call)と[Client.CallForCount](https://godoc.org/github.com/hnakamur/go-zabbix#Client.CallForCount)の2つでカバーする想定です。といいつつ、実際カバーできるか確認はしていません。
[Method reference](https://www.zabbix.com/documentation/2.4/manual/api/reference)を見ると膨大な数があるので、使う必要に迫られたときに都度確認してカバーできなければ実装を改修すれば良いという考えです。

[Client.Call](https://godoc.org/github.com/hnakamur/go-zabbix#Client.Call)と別に[Client.CallForCount](https://godoc.org/github.com/hnakamur/go-zabbix#Client.CallForCount)を作った理由は、個数だけのレスポンスを受け取るためです。

[host.get](https://www.zabbix.com/documentation/2.4/manual/api/reference/host/get)などのAPIでは `countOutput` パラメータに `true` を指定すると `{"jsonrpc":"2.0","result":"3","id":3}` のように `result` には個数だけを返します。

この値が数値だったら https://play.golang.org/p/-sicHGYu7j のようにしてintの変数受け取れるのですが、 https://play.golang.org/p/4WLOGd6pxc のように文字列をintの変数で受け取ろうとすると `json: cannot unmarshal string into Go value of type int` というエラーになってしまいます。

もちろん、 https://play.golang.org/p/pY2RC6Z_Pe のように文字列の変数で受け取る手はあるのですが、その後 [strconv.ParseInt](https://golang.org/pkg/strconv/#ParseInt) で変換する一手間が必要になります。

そこでこの場合だけ https://play.golang.org/p/zju8xBwsED のように `response` 構造体の `Result` フィールドを `int64` 型にして受け取るようにしました。

実際のコードでは [`Result` フィールドを除いた部分を `responseCommon` という構造体として定義し](https://github.com/hnakamur/go-zabbix/blob/b84aa2d4d763287238d45ddac80e54eb1dd66640/client.go#L125-L129)、それに [`Result` フィールドを追加した構造体の変数を Anonymous structs で定義して使っています](https://github.com/hnakamur/go-zabbix/blob/b84aa2d4d763287238d45ddac80e54eb1dd66640/client.go#L161-L198)。


### タイムスタンプの変換
[Appendix 1. Reference commentary](https://www.zabbix.com/documentation/2.4/manual/api/reference_commentary)の `timestamp` は Unix のタイムスタンプ (1970年1月1日0時0分0秒からの通算秒) の文字列形式となっています。

そこで `type Timestamp time.Time` で独自の [`Timestamp`](https://github.com/hnakamur/go-zabbix/blob/b84aa2d4d763287238d45ddac80e54eb1dd66640/types.go) 型を定義し `MarshalJSON` と `UnmarshalJSON` を実装しています。


## この手法の利点

### ライブラリ実装を省力化出来る

例えば[history.get](https://www.zabbix.com/documentation/2.4/manual/api/reference/history/get) APIの戻り値である[History object](https://www.zabbix.com/documentation/2.4/manual/api/reference/history/object)はヒストリのデータ型に応じてFloat historyやInteger historyなど5種類の定義があります。

結果を戻り値で返すようにすると、Goのメソッドも `GetFloatHistory`, `GetIntegerHistory` のように分ける必要が出てきて面倒です。

この手法ならGoの1つのメソッドで多くのAPIをカバーすることが出来るので、コード量が少なくてすみますし、APIの変更にも影響を受けにくいので保守も楽になると思います。

### ライブラリ利用者が必要なフィールドだけを持つ型を定義して使うので省メモリ

`Call` のシグネチャは
`func (c *Client) Call(method string, params, result interface{}) error`
となっていて、 `interface{}` 型なので、これだけ見ても何を渡して良いかわからないという欠点はあります。

ですが、ライブラリ側で真面目に型を定義して回るのも大変です。例えば[Host object](https://www.zabbix.com/documentation/2.4/manual/api/reference/host/object)を見ても、フィールドの数がとても多いです。APIが変更される度にGoの構造体も追随して更新するのはライブラリの保守が大変です。

それに、そんなに苦労してきちんと構造体を定義してみても、ライブラリの利用者にとってはほとんどのフィールドは不要なのです。それなのにライブラリが提供する構造体のサイズが大きいと、メモリの無駄遣いになってしまいます。

例えば[go-zabbix/cmd/example/main.go](https://github.com/hnakamur/go-zabbix/blob/master/cmd/example/main.go)内の `getHostID` 関数では `HostID` フィールドのみの構造体の変数を定義して受け取っています。以下のような感じです。これでメモリの無駄遣いを回避できます。

https://github.com/hnakamur/go-zabbix/blob/6d6f3c0e41f4c0f0bd2061ed85b1237da7ad6a47/cmd/example/main.go#L113-L142

```go:
func getHostID(client *zabbix.Client, hostGroupID, hostname string) (hostID string, err error) {
	type filter struct {
		Host string `json:"host"`
	}
	params := struct {
		GroupIDs string   `json:"groupids"`
		Filter   filter   `json:"filter"`
		Output   []string `json:"output"`
	}{
		GroupIDs: hostGroupID,
		Filter: filter{
			Host: hostname,
		},
		Output: []string{"hostid"},
	}

	var hosts []struct {
		HostID string `json:"hostid"`
	}
	err = client.Call("host.get", params, &hosts)
	if err != nil {
		return
	}
	if len(hosts) == 0 {
		err = errNotFound
		return
	}
	hostID = hosts[0].HostID
	return
}
```

また、Zabbix APIでは[Common "get" method parameters](https://www.zabbix.com/documentation/2.4/manual/api/reference_commentary#common_get_method_parameters)で説明されていますが `output` プロパティに欲しいフィールド名を指定するとそのフィールドだけを取得できるようになっています。必要なフィールドだけ取得するほうが、データ転送量が少なくてすむので活用したいところです。上記の例では `output` パラメータに `hostid` を指定しています。

ところで、ここではリクエストのパラメータを作るのに anonymous structs の手法に加えて、ネストした構造体の部分を `filter` 構造体として関数内で定義して使っています。今回知ったのですが関数内でも型定義出来るんですね。[Declarations and scope](https://golang.org/ref/spec#Declarations_and_scope)を見てもGo言語の文法上正しいようです。

利用者側でアプリケーションのニーズに応じて上記のような関数を定義してから使えば、関数のシグネチャもわかりやすいので使い勝手は悪く無いと思います。

## この手法の限界

この手法はリクエストに応じてレスポンスの構造が決まる場合はカバーできます。が、レスポンスの一部に依存して、残りの部分の構造が変わる場合はカバーできません。その場合は可変部分を `map[string]interface{}` にするか `json.RawMessage` にして別途デコードするかの対応が必要になるかと思います。

## おわりに
かなり良い感じだと思っていますが、いかがでしょう。他では見たことなかったのですが、みなさんご存知でしたでしょうか。