はじめに

前回の記事の続きです。
前回の記事はこちら

テキスト処理

前回同様テキスト処理についてやっていきます。
JSONの処理をやっていきます。

JSONの処理

JSON に関してはよくあるデータの形式ですね。

JSON(Javascript Object Notation)は軽量なデータ記述言語です。文字を基礎とした言語のテキスト形式で、C言語ファミリーに似た習慣を採用しています。

Web開発では特にAPIのレスポンスに使用されていますね。

JSONの解析

構造体に解析する

今回も同様に構造体にて解析できるようです。
早速やっていきましょう。
XML と同様に func Unmarshal(data []byte, v interface{}) error があるようですね。

環境はいつものアレです。
サンプルコードを書いてみました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"encoding/json"
	"fmt"
)

type Server struct {
	ServerName string
	ServerIP   string
}

type Serverslice struct {
	Servers []Server
}

func main() {
	var s Serverslice
	str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
	json.Unmarshal([]byte(str), &s)
	fmt.Println(s)
	fmt.Println(s.Servers[0].ServerName)
	fmt.Println(s.Servers[0].ServerIP)
}

出力結果は下記になります。

1
2
3
4
bash-5.0# go run json.go
{[{Shanghai_VPN 127.0.0.1} {Beijing_VPN 127.0.0.2}]}
Shanghai_VPN
127.0.0.1

上手く解析できていそうです!

中でどういうふうに解析しているのでしょう、プログラムの解説を見てみます。

  • まずtagに含まれるFooのエクスポート可能なstructフィールド(頭文字が大文字)を探し出します。
  • 次にフィールド名がFooのエクスポートフィールドを探し出します。
  • 最後にFOOまたはFoOのような頭文字を除いたその他の大文字小文字を区別しないエクスポートフィールドを探し出します。

そういえば、今回は構造体に tag がないですね。(XMLはtagで囲むからあったわけですが、json では key:valueの形なのでないわけですね。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
	"servers": [ // struct ServerSliceのServers []Serverにマッチする
		{
			"serverName": "Shanghai_VPN", // struct Serverの ServerNameにマッチする
			"serverIP": "127.0.0.1" // struct Serverの ServerIPにマッチする
		},
		{
			"serverName": "Beijing_VPN",
			"serverIP": "127.0.0.2"
		}
	]
}

コメントを入れてみました、例ですと上記がマッチしているわけですね。(2個目の要素の解析は省略しました。)

聡明なあなたであればお気づきかもしれません:代入されうるフィールドはエクスポート可能なフィールドである必要があります。
(すなわち、頭文字が大文字であるということです。)同時にJSONを解析する際探しだせるフィールドを解析するのみで、探せないフィールドについては無視されます。 これのメリットは:とても大きなJSONデータ構造を受け取ってしまいその中のいち部分のデータだけを取得したいような場合です。
あなたは必要なデータに対応するフィールド名の大文字だけで簡単にこの問題を解決することができます。

なるほど、巨大JSONファイルだと、必要な部分の構造体だけ定義することで部分的に取得できたりするわけですね。

interfaceに解析する

上のような解析方法は解析されるJSONデータの構造を事前に知っている場合に採用されるソリューションです。もし解析されるデータの形式が事前に分からなかった場合はどのように解析すればよいでしょうか?

なるほど、確かに?

我々はinterface{}に任意のデータ型のオブジェクトを保存できることを知っています。このようなデータ構造は未知の構造のjsonデータの解析結果を保存するのにぴったりです。 JSONパッケージではmap[string]interface{}と[]interface{}構造を採用して任意のJSONオブジェクトと配列を保存します。Goの型とJSONの型の対応関係は以下の通りです

PHPの連想配列っぽい形になりそうですね。

サンプルコード例がありますね。
このあたりをプログラムにしてみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
	var f interface{}
	err := json.Unmarshal(b, &f)
	if err != nil {
		panic("error can't analyze json unmarshal.")
	}
	m := f.(map[string]interface{})
	for k, v := range m {
		switch vv := v.(type) {
		case string:
			fmt.Println(k, "is string", vv)
		case int:
			fmt.Println(k, "is int", vv)
		case float64:
			fmt.Println(k, "is float64", vv)
		case []interface{}:
			fmt.Println(k, "is an array:")
			for i, u := range vv {
				fmt.Println(i, u)
			}
		default:
			fmt.Println(k, "is of a type I don't know how to handle")
		}
	}
}

出力結果は下記になります。

1
2
3
4
5
6
bash-5.0# go run json2.go 
Name is string Wednesday
Age is float64 6        
Parents is an array:    
0 Gomez
1 Morticia

解析できていますね。
解説見ると、解析した段階では map[string]interface{} 型になり、
この後型のアサーションをして、値を使用できるようになるみたいです。
json だと構造がシンプルなので、解析も複雑にならないですね。

これはオフィシャルが提供するソリューションです。
実は多くの場合、型のアサーションは操作からしてあまり便利ではありません。
現在bitly社ではsimplejsonと呼ばれるパッケージがオープンに開発されています。未知の構造体のJSONを処理する場合にとても便利です。

型アサーションを一々やってられないという気持ちはありますね、
これはどうやら、 ~~.Get("key").Get("nestKey").Array() みたいな感じでメソッドチェーンで取得できるものらしいですね。
便利そうです。
~~.Get("key").Get("nestKey").Array() ← 詳細を見ていないのですが、メソッドチェーンの途中で key がなかった場合はどういう処理になっているのでしょう。
たぶん error が返されると思うのですが、メソッドチェーンの処理は途中で終わらないのでerrが入ってたら処理しないとか?なのかなぁ。
ライブラリを使ってみないとわからなさそうです、今度使ってみます。

JSONを生成する

多くのアプリケーションを開発する際、最後はJSONデータ文字列を出力する必要があります。ではどのようにして処理するのでしょうか?JSONパッケージではMarshal関数を通して処理します。関数の定義は以下の通り:

Marshal 関数ですね、XMLと同じやつです。
func Marshal(v interface{}) ([]byte, error) ですね。(いつもの)
サンプルコードを早速動かしてみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"encoding/json"
	"fmt"
)

type Server struct {
	ServerName string
	ServerIP   string
}

type Serverslice struct {
	Servers []Server
}

func main() {
	var s Serverslice
	s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
	s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
	b, err := json.Marshal(s)
	if err != nil {
		fmt.Println("json err:", err)
	}
	fmt.Println(string(b))
}

出力結果は下記になります。

1
2
bash-5.0# go run json3.go 
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}

上手く出力できていそうです。
構造体→JSONの形ですね。

上で出力されたフィールド名の頭文字はどれも大文字です。
もし頭文字に小文字を使いたい場合はどうすればよいでしょうか?
構造体のフィールド名の頭文字を小文字にすべきでしょうか?
JSONで出力する際に注意が必要なのは、エクスポートされたフィールドのみが出力されるということです。
もしフィールド名を修正してしまうと、何も出力されなくなってしまいます。
ですので必ずstruct tagによって定義した上で実装する必要があります

1
2
3
4
5
6
7
8
type Server struct {
    ServerName string `json:"serverName"`
    ServerIP   string `json:"serverIP"`
}

type Serverslice struct {
    Servers []Server `json:"servers"`
}

なるほど、 この場合はタグが必要なんですね。

先程のサンプルを直して、出力してみます。

1
2
bash-5.0# go run json3.go 
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}

小文字になっていますね。

そして、またサンプルがあるのでこれも実行してみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"encoding/json"
	"os"
)

type Server struct {
	// ID はJSONの中にエクスポートされません。
	ID int `json:"-"`

	// ServerName2 の値は二次JSONエンコーディングが行われます。
	ServerName  string `json:"serverName"`
	ServerName2 string `json:"serverName2,string"`

	// もし ServerIP が空であれば、JSON文字列の中には出力されません。
	ServerIP string `json:"serverIP,omitempty"`
}

func main() {
	s := Server{
		ID:          3,
		ServerName:  `Go "1.0" `,
		ServerName2: `Go "1.0" `,
		ServerIP:    ``,
	}
	b, _ := json.Marshal(s)
	os.Stdout.Write(b)
}

出力結果です。

1
2
bash-5.0# go run json4.go 
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}bash-5.0# 

しっかり構造体に定義したタグと同じようになっていますね。
(省略とかされてます。)

JSONオブジェクトはstringのみをkeyとしてサポートします。そのためmapをエンコードしたい場合は必ずmap[string]Tのような型となります。(TはGo言語の中の任意の型です。)
Channel, complexとfunctionはJSONにはエンコードされません。
ネストしたデータはエンコードされません。さもないとJSONのエンコードは永久ループに入ってしまいます。
ポインタがエンコードされる際はポインタが指定している内容が出力されます。空のポインタはnullを出力します。

なるほど。

ネストしたデータはエンコードされません。さもないとJSONのエンコードは永久ループに入ってしまいます。

これが気になりますね、これはつまり構造体の中に構造体があった場合はエンコードされないってことでしょうか。
気になるのでやってみます。

エンコード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
	"encoding/json"
	"fmt"
)

type Option struct {
	Description string
}

type Server struct {
	ServerName  string `json:"serverName"`
	ServerIP    string `json:"serverIP"`
	OptionField Option
}

type Serverslice struct {
	Servers []Server `json:"servers"`
}

func main() {
	var s Serverslice
	s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1", OptionField: Option{"description1"}})
	s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2", OptionField: Option{"description2"}})
	b, err := json.Marshal(s)
	if err != nil {
		fmt.Println("json err:", err)
	}
	fmt.Println(string(b))
}

出力結果

1
2
bash-5.0# go run json6.go 
{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1","OptionField":{"Description":"description1"}},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2","OptionField":{"Description":"description2"}}]}

見た感じできていそうだが・・・。
よしわからん(投げやり)

解析(デコードの入れ子)
解析させるJSON

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
	"servers": [
		{
			"serverName": "Shanghai_VPN",
			"serverIP": "127.0.0.1",
			"OptionField": {
				"Description": "desc"
			}
		},
		{
			"serverName": "Beijing_VPN",
			"serverIP": "127.0.0.2"
		}
	]
}

コード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"encoding/json"
	"fmt"
)

type Option struct {
	Description string
}

type Server struct {
	ServerName  string
	ServerIP    string
	OptionField Option
}

type Serverslice struct {
	Servers []Server
}

func main() {
	var s Serverslice
	str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1", "OptionField": {"Description": "desc"}},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
	json.Unmarshal([]byte(str), &s)
	fmt.Println(s)
	fmt.Println(s.Servers[0].ServerName)
	fmt.Println(s.Servers[0].ServerIP)
	fmt.Println(s.Servers[0].OptionField.Description)
}
1
2
3
4
5
bash-5.0# go run json5.go 
{[{Shanghai_VPN 127.0.0.1 {desc}} {Beijing_VPN 127.0.0.2 {}}]}
Shanghai_VPN
127.0.0.1
desc

参考

Unmarshaling nested JSON objects in Golang - https://stackoverflow.com/questions/21268000/unmarshaling-nested-json-objects-in-golang

おわりに

次回 「正規表現の処理」