WebAPIを利用したコードをgolangで書く場合など、APIのレスポンスをStructにUnmarshalすることはよくあります。
基本
以下の様なjsonを返すサーバーを書く。
{
name: 'hoge',
age: 21
}
サーバー側の実装。
localhost:8000にアクセスするとTwitterのStreamingAPIのように延々とレスポンスを返している。
ref:http://qiita.com/kyokomi/items/cf1dd4fa85f1a99a42cc
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", Streaming)
http.ListenAndServe(":8000", nil)
}
func Streaming(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
for {
u := new(User)
u.Name = "a"
u.Age = 22
enc, _ := json.Marshal(u)
w.Write(enc)
w.Write([]byte("\n"))
w.(http.Flusher).Flush()
time.Sleep(1 * time.Second)
}
}
type User struct {
Name string
Age int64
}
これを受ける側のクライアントのコードはこんな感じ。
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/k0kubun/pp"
)
func main() {
res, err := http.Get("http://localhost:8000")
if err != nil {
log.Fatalln(err)
}
scanner := bufio.NewScanner(res.Body)
for {
if ok := scanner.Scan(); !ok {
break
}
u := new(User)
if err = json.Unmarshal(scanner.Bytes(), &u); err != nil {
break
}
pp.Print(u)
}
}
type User struct {
Name string
Age int64
}
単純に延々と受けて標準出力に出すだけ。
異なる形式のJSONがランダムに返される場合
ここからが本旨。
ランダムに別の形のJSONが返される場合はどうすればいいか。
フィールドなどが全く違うJSONが返されるので先ほどのUserという型のStructではUnmarshal出来ません。
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", Streaming)
http.ListenAndServe(":8000", nil)
}
func Streaming(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
for {
rand.Seed(time.Now().Unix())
rand := rand.Intn(2)
if rand == 0 {
u := new(User)
u.Name = "a"
u.Age = 22
enc, _ := json.Marshal(u)
w.Write(enc)
} else {
e := new(Event)
e.Title = "title"
enc, _ := json.Marshal(e)
w.Write(enc)
}
w.Write([]byte("\n"))
w.(http.Flusher).Flush()
time.Sleep(1 * time.Second)
}
fmt.Fprintf(w, "hello end\n")
}
type User struct {
Name string
Age int64
}
type Event struct {
Title string
}
Userという型とEventという型のjsonが返ります。
このような場合、クライアント側はどうすればいいでしょうか。
interfaceで受けてフィールド文字列で判別してUnmarshalする
StructにUnmarshalする前に一旦interfaceにUnmarshalすることで、どんなjsonでも受ける事ができます。
そして、その受けたinterfaceにどのフィールドがあるかを判定し、そのレスポンスに合ったStructにUnmarshalすれば良いです。
ソース全文は以下.
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/k0kubun/pp"
)
func main() {
res, err := http.Get("http://localhost:8000")
if err != nil {
log.Fatalln(err)
}
scanner := bufio.NewScanner(res.Body)
for {
if ok := scanner.Scan(); !ok {
break
}
var result interface{}
if err = json.Unmarshal(scanner.Bytes(), &result); err != nil {
fmt.Println(err)
continue
}
msg := result.(map[string]interface{})
if _, ok := msg["Name"]; ok {
u := new(User)
if err = json.Unmarshal(scanner.Bytes(), &u); err != nil {
break
}
pp.Print(u)
}
if _, ok := msg["Title"]; ok {
e := new(Event)
if err = json.Unmarshal(scanner.Bytes(), &e); err != nil {
break
}
pp.Print(e)
}
}
}
type User struct {
Name string
Age int64
}
type Event struct {
Title string
}
要は、jsonをinterfaceで取得して"Name"というフィールドがあればUser型のStruct,"Title"というフィールドがあればEventというStructへUnmarshalしている。
var result interface{}
if err = json.Unmarshal(scanner.Bytes(), &result); err != nil {
fmt.Println(err)
continue
}
msg := result.(map[string]interface{})
if _, ok := msg["Name"]; ok {
u := new(User)
if err = json.Unmarshal(scanner.Bytes(), &u); err != nil {
break
}
pp.Print(u)
}
if _, ok := msg["Title"]; ok {
e := new(Event)
if err = json.Unmarshal(scanner.Bytes(), &e); err != nil {
break
}
pp.Print(e)
}
動作はこんな感じになる。
TwitterのUserStreamは異なるJSONが延々吐かれるのでこんな感じで判別するのが良さそう。
2度Unmarshalするので非効率さはありますので、もっと良い方法を知っていれば教えて下さい。