Go言語でJSONを扱う

  • 19
    Like
  • 0
    Comment

この記事は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関数を使います。

json.Unmarshal
func Unmarshal(data []byte, v interface{}) error

GoでJSONを扱う上で面白いと思ったのは、定義した構造体にそのままデータを放り込めるという点です。

実際にJSONデコードを行うコードを例に見てみましょう。

vro.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"}
]
vro.go
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)
    }
}
result
1 : akane
2 : aoi
3 : wakaba
4 : himawari
0 : rei

PHPなら5行で書けそうな処理だね。
すごくかんたん!!!

Goでの基本的なJSONデコード処理の書き方は

  1. 構造体を定義する
  2. ファイルやWebAPI等からJSON文字列を取得する
  3. json.UnmarshalでJSON文字列をデコードし、結果を構造体変数に放り込む
  4. 構造体変数のデータを処理をする

となります。

そもそもjson.Unmarshalのデコード結果はinterfaceで返ってくるので、構造体の定義は必須ではないのですが、GoでJSONを扱う上では使った方がよいです。

interfaceをそのまま展開した場合、map[string]interface{}となる
[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]]
参考までにinterface型からデータを取り出す方法
    // 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デコードエラー
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のフィールド名と必ずしも一致している必要はありません。

必要な分だけ定義すればよい

すべてのフィールドを扱うわけではない場合、構造体のフィールドは省略しても問題ありません。

IDとNameしか使わない場合の定義
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"`
}
Colorを出力するように修正
    // 表示
    for _, p := range persons {
        fmt.Printf("%d : %s (%s)\n", p.Id, p.Name, p.Vivid.Color)
    }
result
1 : akane (red)
2 : aoi (blue)
3 : wakaba (green)
4 : himawari (yellow)
0 : rei ()

必須項目ではないフィールド定義

上記の例のJSON(vro.json)では"birthday"や"vivid_info"が一部フィールドが欠けているレコードがありますが、パースエラーとはなりません。
この場合、フィールドがない項目は型の初期値(数値系なら0、文字列系なら空文字)が設定されます。

Birthdayを出力するように修正
    // 表示
    for _, p := range persons {
        fmt.Printf("%d : %s [%s]\n", p.Id, p.Name, p.Birthday)
    }
result
1 akane [08-16]
2 aoi [06-17]
3 wakaba [05-22]
4 himawari [07-23]
0 rei [] <- JSONにbirthdayフィールドがないのでstring型の初期値の空文字が設定される

これだとBirthdayに設定された空文字が「初期値の空文字」であるのか「意味のある空文字」であるのかの区別がつかなくなってしまいます。そういう場合はポインタで定義してあげるといいでしょう。

Birthdayをstringのポインタで定義
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)
        }
    }
result
1 : akane (08-16)
2 : aoi (06-17)
3 : wakaba (05-22)
4 : himawari (07-23)
0 : rei

構造体への関数定義もできる

構造体の機能はそのまま使えます。

Person構造体に関数定義を追加
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())
    }
result
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が取得できます。

Aさんのツイート
{
  "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の構造体で扱う場合、以下のような定義になります。

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"`
}

さらには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)
}
Twitterから取得したJSONを表示する例
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())
        }
    }
}
result
Aさん : 私はAさんです。
Aさん : 私はAさんです。 [retweeted by Bさん]

ね、簡単でしょ?