Go基础:常见的json包–encoding/json、easyjson、ffjson、json-iterator/go
本文对常见的json包做一些介绍,方便快速入门。每一小节均有示例说明。大家在实际开发中可以选择适合自己的json包。
encoding/json
encoding/json是官方提供的标准json, 实现RFC 7159中定义的JSON编码和解码。
使用的时候需要预定义struct,原理是通过reflection和interface来完成工作, 性能低。
常用的接口:
func Marshal(v interface{}) ([]byte, error) 生成JSON
func 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"}{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就很方便了。