本文對常見的json包做一些介紹,方便快速入門。每一小節均有示例說明。大家在實際開發中可以選擇適合自己的json包。
encoding/json
encoding/json
是官方提供的標准json, 實現RFC 7159中定義的JSON編碼和解碼。使用的時候需要預定義struct
,原理是通過reflection
和interface
來完成工作, 性能低。
常用的接口:
func Marshal(v interface{}) ([]byte, error)
生成JSONfunc Unmarshal(data []byte, v interface{}) error
解析JSON到struct
示例1 生成JSON:
type ColorGroup struct {
ID int
Name string
Colors []string
}
group := ColorGroup{
ID: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
b, err := json.Marshal(group)
if err != nil {
fmt.Println("error:", err)
}
os.Stdout.Write(b)
Output:
{"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}
示例2 解析JSON:
var jsonBlob = []byte(`[
{"Name": "Platypus", "Order": "Monotremata"},
{"Name": "Quoll", "Order": "Dasyuromorphia"}
]`)
type Animal struct {
Name string
Order string
}
var animals []Animal
err := json.Unmarshal(jsonBlob, &animals)
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%+v", animals)
Output:
[{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]
easyjson, ffjson
標准庫性能的瓶頸在反射。easyjson, ffjson 並沒有使用反射方式實現,而是在Go中為結構體生成靜態MarshalJSON
和UnmarshalJSON
函數,類似於預編譯。調用編碼解碼時直接使用生成的函數,從而減少了對反射的依賴,所以通常快2到3倍。但相比標准JSON包,使用起來略為繁瑣。
使用步驟:
1、定義結構體,每個結構體注釋里標注 //easyjson:json
或者 //ffjson: skip
;
2、使用 easyjson
或者ffjson
命令將指定目錄的go結構體文件生成帶有Marshal
、Unmarshal
方法的新文件;
3、代碼里如果需要進行生成JSON或者解析JSON,調用生成文件的 Marshal
、Unmarshal
方法即可。
下面是使用示例。
easyjson
GitHub:https://github.com/mailru/easyjson
1、先安裝:
go get -u github.com/mailru/easyjson/
2、定義結構體:
記得在需要使用easyjson
的結構體上加上//easyjson:json
。 如下:
//easyjson:json
type School struct {
Name string `json:"name"`
Addr string `json:"addr"`
}
//easyjson:json
type Student struct {
Id int `json:"id"`
Name string `json:"s_name"`
School School `json:"s_chool"`
Birthday time.Time `json:"birthday"`
}
3、在結構體包下執行
easyjson -all student.go
此時在該目錄下出現一個新的文件:easyjson_student.go,該文件給結構體增加了MarshalJSON
、UnmarshalJSON
等方法。
4、使用
package main
import (
"studygo/easyjson"
"time"
"fmt"
)
func main(){
s:=easyjson.Student{
Id: 11,
Name:"qq",
School:easyjson.School{
Name:"CUMT",
Addr:"xz",
},
Birthday:time.Now(),
}
bt,err:=s.MarshalJSON()
fmt.Println(string(bt),err)
json:=`{"id":11,"s_name":"qq","s_chool":{"name":"CUMT","addr":"xz"},"birthday":"2017-08-04T20:58:07.9894603+08:00"}`
ss:=easyjson.Student{}
ss.UnmarshalJSON([]byte(json))
fmt.Println(ss)
}
運行結果:
{"id":11,"s_name":"qq","s_chool":{"name":"CUMT","addr":"xz"},"birthday":"2017-08-04T20:58:07.9894603+08:00"} <nil>
{121 {CwwwwwwwUMT xzwwwww} 2017-08-04 20:52:03.4066002 +0800 CST}
ffjson
GitHub:https://github.com/pquerna/ffjson
本小節就不給示例了,大家直接看github上說明。用法與easyjson類似。
需要注意的是,ffjson也提供了ffjson.Marshal
和ffjson.Unmarshal
方法,如果沒有使用ffjson給對應結構體生成靜態的方法,則會調用標准庫encoding/json
進行編碼解碼:
func Marshal(v interface{}) ([]byte, error) {
//調用結構體的靜態方法
f, ok := v.(marshalerFaster)
if ok {
buf := fflib.Buffer{}
err := f.MarshalJSONBuf(&buf)
b := buf.Bytes()
if err != nil {
if len(b) > 0 {
Pool(b)
}
return nil, err
}
return b, nil
}
//調用encoding/json
j, ok := v.(json.Marshaler)
if ok {
return j.MarshalJSON()
}
return json.Marshal(v)
}
json-iterator/go
這是一個很神奇的庫,滴滴開發的,不像 easyjson 和 ffjson 都使用了預編譯,而且 100% 兼容encoding/json
的高性能json庫。根據作者的壓測介紹,該庫比easyjson
性能還要好那么一點點(有點懷疑~)。
json-iterator使用
modern-go/reflect2
來優化反射性能。然后就是通過大幅度減少反射操作,來提高速度。
Github: https://github.com/json-iterator/go
首先來看下用法:
標准庫寫法:
import "encoding/json"
json.Marshal(&data)
json-iterator寫法:
import "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibrary
json.Marshal(&data)
此外,該庫還提供了個功能,對於從PHP轉過來的朋友很有幫助。
PHP是弱類型,所以接口里經常把數字10寫成字符串”10″返回,導致一個表達年齡的JSON變成了這樣:
{
"age": "10"
}
golang標准庫的json並不能兼容這種情況下的解析,因此如果我們的struct
企圖使用int
來反射這個字段,將會導致decode
失敗。此時json-iterator/go
就派上用場了:
package main
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/json-iterator/go/extra"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
func init() {
// RegisterFuzzyDecoders decode input from PHP with tolerance.
// It will handle string/number auto conversation, and treat empty [] as empty struct.
extra.RegisterFuzzyDecoders()
}
type StdStruct struct {
Age int `json:"age"`
}
func main() {
s := `{"age": "10"}`
d := &StdStruct{}
if err := json.Unmarshal([]byte(s), d); err != nil {
fmt.Println(err)
} else {
fmt.Println(d.Age)
}
}
輸出:
10
們只需要在main文件里的init
方法中開啟1次PHP兼容模式即可,后續引入的模塊不需要重復開啟。
go-simplejson, gabs, jason
這幾個包都是在encoding/json
的基礎上進行開發的,為了是更方便的操作JSON:它不需要創建struct,而是動態按字段取內容。它們大部分只是一個解析庫,並沒有序列化的接口。有時候我們僅僅想取JSON里的某個字段,用這個非常有用。
下面是go-simplejson
示例。
go-simplejson
Github: https://github.com/bitly/go-simplejson
package main
import (
"fmt"
"github.com/bitly/go-simplejson"
)
func main() {
data := []byte(`{
"hits":{
"total":2,
"max_score":4.631368,
"hits":[
{
"_source":{
"account_number":298,
"balance":34334,
"firstname":"Bullock",
"lastname":"Marsh"
}
}
]
}
}`)
js, _ := simplejson.NewJson(data)
//get total
total, _ := js.Get("hits").Get("total").Int64()
fmt.Println(total)
account_number, _ := js.Get("hits").Get("hits").GetIndex(0).Get("_source").Get("account_number").Int64()
fmt.Println(account_number)
//get _source list
hitsjson, _ := js.Get("hits").Get("hits").MarshalJSON()
fmt.Printf("%s", hitsjson)
}
輸出:
2
298
[{"_id":"298","_index":"bank","_score":4.631368,"_source":{"account_number":298,"balance":34334,"firstname":"Bullock","lastname":"Marsh"},"_type":"account"}]
go-simplejson 沒有提供類似Each方法,無法對數組類型的進行遍歷。但是我們可以將數組取到后調用MarshalJSON
生成JSON,使用標准的encoding/json
進行解析。
gabs
Github: https://github.com/Jeffail/gabs
package main
import (
"fmt"
"github.com/Jeffail/gabs/v2"
)
func main() {
data := []byte(`{}`) //注:為節省篇幅,data結構參考go-simplejson
js, _ := gabs.ParseJSON(data)
//get total
var total float64
//使用斷言,否則類型錯誤會報錯
if val, ok := js.Path("hits.total").Data().(float64); ok {
total = val
}
total2 := js.Search("hits", "total").Data().(float64)
total3 := js.S("hits", "total").Data().(float64) // S is shorthand for Search
gObj, _ := js.JSONPointer("/hits/total")
total4 := gObj.Data().(float64)
fmt.Println(total, total2, total3, total4)
exist := js.Exists("hits", "total")
fmt.Println(exist)
account_number := js.Path("hits.hits.0._source.account_number").Data().(float64)
fmt.Println(account_number)
//Iterating arrays
for _, v := range js.S("hits", "hits").Children() {
lastname := v.S("_source", "lastname").Data().(string)
fmt.Printf("%v\n", lastname)
}
}
輸出:
2 2 2 2
true
298
Marsh
除此之外,gabs 還支持重新動態生成JSON、合並JSON等操作。但是解析需要使用斷言這一點不是很方便。
jason
Github: https://github.com/antonholmquist/jason
示例:
package main
import (
"fmt"
"github.com/antonholmquist/jason"
)
func main() {
data := []byte(`{}`) //注:為節省篇幅,data結構參考go-simplejson
js, _ := jason.NewObjectFromBytes(data)
//get total
total, _ := js.GetInt64("hits", "total")
fmt.Println(total)
//get _source list
hitsjson, _ := js.GetObjectArray("hits", "hits")
for _, v := range hitsjson {
lastname, _ := v.GetString("_source", "lastname")
fmt.Printf("%v\n", lastname)
}
}
輸出:
2
Marsh
提供了遍歷數組的方法,但是沒有提供按索引取某個數組的方法。
jsonparser
jsonparser 功能與go-simplejson
類似,只是一個解析庫,並沒有序列化的接口。但是由於底層不是基於encoding/json
開發的,官方宣稱它比encoding/json
快10倍。
GitHub: https://github.com/buger/jsonparser
下面是個解析ES的示例:
package main
import (
"encoding/json"
"fmt"
"github.com/buger/jsonparser"
)
type UserInfo struct {
AccountNumber int64 `json:"account_number"`
Balance int64 `json:"balance"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
}
func main() {
data := []byte(`{}`) //注:為節省篇幅,data結構參考go-simplejson
//get total
total, _ := jsonparser.GetInt(data, "hits", "total")
fmt.Println(total)
//get _source list
var list []UserInfo
hitsjson, _, _, _ := jsonparser.Get(data, "hits", "hits")
type hitsMap struct {
Source UserInfo `json:"_source,omitempty"`
}
var hitsMaps []hitsMap
json.Unmarshal(hitsjson, &hitsMaps)
for _, info := range hitsMaps {
list = append(list, info.Source)
}
fmt.Printf("%+v\n", list)
//get each _source
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
_source, _, _, _ := jsonparser.Get(value, "_source")
fmt.Println(string(_source))
}, "hits", "hits")
}
輸出:
2
[{AccountNumber:298 Balance:34334 Firstname:Bullock Lastname:Marsh}]
{
"account_number": 298,
"balance": 34334,
"firstname": "Bullock",
"lastname": "Marsh"
}
大家可以看一下 elastic/go-elasticsearch 給出的示例里是怎么解析JSON的:
// Print the response status, number of results, and request duration.
log.Printf(
"[%s] %d hits; took: %dms",
res.Status(),
int(r["hits"].(map[string]interface{})["total"].(map[string]interface{})["value"].(float64)),
int(r["took"].(float64)),
)
// Print the ID and document source for each hit.
for _, hit := range r["hits"].(map[string]interface{})["hits"].([]interface{}) {
log.Printf(" * ID=%s, %s", hit.(map[string]interface{})["_id"], hit.(map[string]interface{})["_source"])
}
對,就是使用的斷言,這個會讓人很崩潰,萬一值不存在或者類型不對,還會直接扔個ERROR...
總結
大部分情況下大家直接使用 encoding/json
就行了。如果追求極致的性能,考慮 easyjson
。遇到解析ES搜索返回的復雜的JSON或者僅需要解析個別字段, go-simplejson
或者jsonparser
就很方便了。
參考
1、json - GoDoc
https://godoc.org/encoding/json#example-Unmarshal
2、Golang的json包一覽 - 知乎
https://zhuanlan.zhihu.com/p/24451749
3、bitly/go-simplejson: a Go package to interact with arbitrary JSON
https://github.com/bitly/go-simplejson
4、buger/jsonparser: Alternative JSON parser for Go that does not require schema (so far fastest)
https://github.com/buger/jsonparser
5、Golang高性能json包:easyjson - 夢朝思夕的個人空間 - OSCHINA
https://my.oschina.net/qiangmzsx/blog/1503018
6、pquerna/ffjson: faster JSON serialization for Go
https://github.com/pquerna/ffjson
7、Jeffail/gabs: For parsing, creating and editing unknown or dynamic JSON in Go
https://github.com/Jeffail/gabs
8、golang – 利用json-iterator庫兼容解析PHP JSON | 魚兒的博客
https://yuerblog.cc/2019/11/08/golang-利用json-iterator庫兼容解析php-json/
9、json-iterator 使用要注意的大坑 – 萌叔
http://vearne.cc/archives/433
10、golang json 性能分析 - - SegmentFault 思否
https://segmentfault.com/a/1190000013022780