この記事はGo (その3) Advent Calendar 2016の23日目の記事です。
GoでJSONを扱った時えらく感動したので、GoでのJSONの扱い方について書いていきます。
JSONでGo
REST APIなど、WebAPIを利用する上で欠かせないJSON (JavaScript Object Notation)。
GoでJSONを扱う場合、encoding/jsonパッケージを使います。
JSONをデコードするにはUnmarshal関数を使います。
func Unmarshal(data []byte, v interface{}) error
GoでJSONを扱う上で面白いと思ったのは、定義した構造体にそのままデータを放り込めるという点です。
実際にJSONデコードを行うコードを例に見てみましょう。
[
{"id":1,"name":"akane","birthday":"08-16","vivid_info":{"color":"red","weapon":"Rang"}},
{"id":2,"name":"aoi","birthday":"06-17","vivid_info":{"color":"blue","weapon":"Impact"}},
{"id":3,"name":"wakaba","birthday":"05-22","vivid_info":{"color":"green","weapon":"Blade"}},
{"id":4,"name":"himawari","birthday":"07-23","vivid_info":{"color":"yellow","weapon":"Collider"}},
{"id":0,"name":"rei"}
]
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
)
/** JSONデコード用に構造体定義 */
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Birthday string `json:"birthday"`
}
func main() {
// JSONファイル読み込み
bytes, err := ioutil.ReadFile("vro.json")
if err != nil {
log.Fatal(err)
}
// JSONデコード
var persons []Person
if err := json.Unmarshal(bytes, &persons); err != nil {
log.Fatal(err)
}
// デコードしたデータを表示
for _, p := range persons {
fmt.Printf("%d : %s\n", p.Id, p.Name)
}
}
1 : akane
2 : aoi
3 : wakaba
4 : himawari
0 : rei
PHPなら5行で書けそうな処理だね。
すごくかんたん!!!
Goでの基本的なJSONデコード処理の書き方は
- 構造体を定義する
- ファイルやWebAPI等からJSON文字列を取得する
- json.UnmarshalでJSON文字列をデコードし、結果を構造体変数に放り込む
- 構造体変数のデータを処理をする
となります。
そもそもjson.Unmarshalのデコード結果はinterfaceで返ってくるので、構造体の定義は必須ではないのですが、GoでJSONを扱う上では使った方がよいです。
[map[name:akane birthday:08-16 vivid_info:map[color:red weapon:Rang] id:1] map[id:2 name:aoi birthday:06-17 vivid_info:map[color:blue weapon:Impact]] map[id:3 name:wakaba birthday:05-22 vivid_info:map[color:green weapon:Blade]] map[id:4 name:himawari birthday:07-23 vivid_info:map[color:yellow weapon:Collider]] map[id:0 name:rei]]
// JSONデコード
var decode_data interface{}
if err := json.Unmarshal(bytes, &decode_data); err != nil {
log.Fatal(err)
}
// 表示
for _, data := range decode_data.([]interface{}) {
var d = data.(map[string]interface{})
fmt.Printf("%d : %s\n", int(d["id"].(float64)), d["name"])
}
PHPやJavaScriptなどの動的型付け言語に慣れていると、JSONデコード関数をかました瞬間にオブジェクトなり配列が生成されるので、いちいち構造体を定義しないといけないことに不便に感じるかもしれません。
しかし型が保証されるのは大きなメリットになると思います。
構造体に定義された型とJSONデータの型が異なっていた場合に以下のようなエラーを吐きます。
json: cannot unmarshal string into Go value of type int
Goの構造体でJSONデータを扱う
まずは構造体の定義の仕方です。
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Birthday string `json:"birthday"`
}
基本的な構造体定義にプラスして、末尾に`json:"XXX"`を定義するだけです。
XXXはJSONのフィールド名です。構造体のフィールド名はJSONのフィールド名と必ずしも一致している必要はありません。
必要な分だけ定義すればよい
すべてのフィールドを扱うわけではない場合、構造体のフィールドは省略しても問題ありません。
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
}
別の構造体定義も使える
vivid_infoを扱う為に、構造体に定義を追加してみます。
ネストされたJSONデータを扱いには構造体内に別の構造体をネストして書くことも可能ですが、使いまわしが利く別に切り出した構造体で定義した方が後々楽です。
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Birthday string `json:"birthday"`
Vivid struct { // <- 構造体の中にネストさせて構造体を定義
Color string `json:color`
Weapon string `json:weapon`
} `json:"vivid_info"`
}
type VividInfo struct {
Color string `json:color`
Weapon string `json:weapon`
}
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Birthday string `json:"birthday"`
Vivid VividInfo `json:"vivid_info"`
}
// 表示
for _, p := range persons {
fmt.Printf("%d : %s (%s)\n", p.Id, p.Name, p.Vivid.Color)
}
1 : akane (red)
2 : aoi (blue)
3 : wakaba (green)
4 : himawari (yellow)
0 : rei ()
必須項目ではないフィールド定義
上記の例のJSON(vro.json)では"birthday"や"vivid_info"が一部フィールドが欠けているレコードがありますが、パースエラーとはなりません。
この場合、フィールドがない項目は型の初期値(数値系なら0、文字列系なら空文字)が設定されます。
// 表示
for _, p := range persons {
fmt.Printf("%d : %s [%s]\n", p.Id, p.Name, p.Birthday)
}
1 akane [08-16]
2 aoi [06-17]
3 wakaba [05-22]
4 himawari [07-23]
0 rei [] <- JSONにbirthdayフィールドがないのでstring型の初期値の空文字が設定される
これだとBirthdayに設定された空文字が「初期値の空文字」であるのか「意味のある空文字」であるのかの区別がつかなくなってしまいます。そういう場合はポインタで定義してあげるといいでしょう。
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Birthday *string `json:"birthday"` // <- ポインタ定義
Vivid *VividInfo `json:"vivid_info"` // <- ポインタ定義
}
ポインタで定義した場合、フィールドがないとnilが設定されます。
// 表示
for _, p := range persons {
if p.Birthday != nil {
fmt.Printf("%d : %s (%s)\n", p.Id, p.Name, *p.Birthday)
} else {
fmt.Printf("%d : %s\n", p.Id, p.Name)
}
}
1 : akane (08-16)
2 : aoi (06-17)
3 : wakaba (05-22)
4 : himawari (07-23)
0 : rei
構造体への関数定義もできる
構造体の機能はそのまま使えます。
func (p *Person) GetInfo() string {
if p.Birthday != nil {
return fmt.Sprintf("%d : %s [%s]", p.Id, p.Name, *p.Birthday)
}
return fmt.Sprintf("%d : %s", p.Id, p.Name)
}
for _, p := range persons {
fmt.Println(p.GetInfo())
}
1 : akane [08-16]
2 : aoi [06-17]
3 : wakaba [05-22]
4 : himawari [07-23]
0 : rei
TwitterのREST APIで使ってみよう
ちょっと実用的な例として、TwitterのREST APIであるstatuses/home_timelineを利用します。
statuses/home_timelineから取得できるデータは大きく分けて「ツイートデータ」と「ユーザデータ」で構成されています。
Aさんが「私はAさんです。」とTwitterに投稿した場合、ざっくりですが以下のようなJSONが取得できます。
{
"created_at": "Thu Dec 22 15:00:00 +0000 2016",
"text": "私はAさんです。",
"id": 11111,
"user": {
"name": "Aさん",
"id": 1,
"screen_name": "example_user_a_san"
}
}
Goの構造体で扱う場合、以下のような定義になります。
type User struct {
Name string
Id uint64 `json:"id"`
ScreenName string `json:"screen_name"`
}
type Status struct {
CreatedAt string `json:"created_at"`
Text string `json:"text"`
Id uint64 `json:"id"`
User User `json:"user"`
}
さらにはTwitterにはリツイートといった機能があります。
Bさんが先ほどのAさんのツイートをリツイートした場合、APIから以下のようなJSONが取得できます。
{
"created_at": "Thu Dec 22 19:00:00 +0000 2016",
"id": 11222,
"user": {
"name": "Bさん",
"id": 2,
"screen_name": "example_user_b_san"
},
"retweeted_status": {
"created_at": "Thu Dec 22 15:00:00 +0000 2016",
"text": "私はAさんです。",
"id": 11111,
"user": {
"name": "Aさん",
"id": 1,
"screen_name": "example_user_a_san"
}
}
}
Statusの中にretweeted_statusというフィールド名でまるまるStatusと同じ内容が設定されるような感じですね。
これは構造体の埋め込みを使い定義をします。
→ Goで埋め込んだ構造体データを取り出す
過去このやり方がわからず、かな~り悩みました・・・
type ReceivedStatus struct {
Status // <- 埋め込み
RetweetedStatus *Status `json:"retweeted_status"`
}
ReceivedStatusはStatusの構造体をそのまま引き継ぎ、かつポインタ型のStatus構造体を持つことになります。
Status構造体にツイート整形用の関数を定義してみましょう。
func (s *Status) GetText() string {
return fmt.Sprintf("%s : %s", s.User.Name, s.Text)
}
package main
import (
"encoding/json"
"fmt"
"log"
"twitter" // 実際にこのパッケージはありません
)
type User struct {
Name string
Id uint64 `json:"id"`
ScreenName string `json:"screen_name"`
}
type Status struct {
CreatedAt string `json:"created_at"`
Text string `json:"text"`
Id uint64 `json:"id"`
User User `json:"user"`
}
func (s *Status) GetText() string {
return fmt.Sprintf("%s : %s", s.User.Name, s.Text)
}
type ReceivedStatus struct {
Status // <- 埋め込み
RetweetedStatus *Status `json:"retweeted_status"`
}
func main() {
// TwitterAPIからデータを取得する処理
bytes, err := twitter.GetHomeTimeline()
if err != nil {
log.Fatal(err)
}
// JSONデコード
var recieved_statuses []ReceivedStatus
if err := json.Unmarshal(bytes, &recieved_statuses); err != nil {
log.Fatal(err)
}
// デコードしたデータを表示
for _, rs := range recieved_statuses {
if rs.RetweetedStatus != nil {
// リツイートの場合
fmt.Printf("%s [retweeted by %s]", rs.RetweetedStatus.GetText(), rs.User.Name)
} else {
// 通常ツイートの場合
fmt.Println(rs.GetText())
}
}
}
Aさん : 私はAさんです。
Aさん : 私はAさんです。 [retweeted by Bさん]
ね、簡単でしょ?