乐趣区

Golang的json包

本文对常见的 json 包做一些介绍,方便快速入门。每一小节均有示例说明。大家在实际开发中可以选择适合自己的 json 包。

encoding/json

encoding/json是官方提供的标准 json, 实现 RFC 7159 中定义的 JSON 编码和解码。使用的时候需要预定义 struct,原理是通过reflectioninterface来完成工作, 性能低。

常用的接口:

  • 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 中为结构体生成静态 MarshalJSONUnmarshalJSON函数,类似于预编译。调用编码解码时直接使用生成的函数,从而减少了对反射的依赖,所以通常快 2 到 3 倍。但相比标准 JSON 包,使用起来略为繁琐。

使用步骤:

1、定义结构体,每个结构体注释里标注 //easyjson:json或者 //ffjson: skip
2、使用 easyjson或者 ffjson 命令将指定目录的 go 结构体文件生成带有 MarshalUnmarshal 方法的新文件;
3、代码里如果需要进行生成 JSON 或者解析 JSON,调用生成文件的 MarshalUnmarshal方法即可。

下面是使用示例。

easyjson

GitHub:https://github.com/mailru/eas…

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,该文件给结构体增加了 MarshalJSONUnmarshalJSON 等方法。

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/ff…

本小节就不给示例了,大家直接看 github 上说明。用法与 easyjson 类似。

需要注意的是,ffjson 也提供了 ffjson.Marshalffjson.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-itera…

首先来看下用法:

标准库写法:

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-s…

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/antonholmq…

示例:

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/json…

下面是个解析 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/js…
2、Golang 的 json 包一览 – 知乎
https://zhuanlan.zhihu.com/p/…
3、bitly/go-simplejson: a Go package to interact with arbitrary JSON
https://github.com/bitly/go-s…
4、buger/jsonparser: Alternative JSON parser for Go that does not require schema (so far fastest)
https://github.com/buger/json…
5、Golang 高性能 json 包:easyjson – 梦朝思夕的个人空间 – OSCHINA
https://my.oschina.net/qiangm…
6、pquerna/ffjson: faster JSON serialization for Go
https://github.com/pquerna/ff…
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/0…
9、json-iterator 使用要注意的大坑 – 萌叔
http://vearne.cc/archives/433
10、golang json 性能分析 – – SegmentFault 思否
https://segmentfault.com/a/11…

本文首发于公众号 ” 飞鸿影的博客(fhyblog)”,欢迎关注。博客地址:https://52fhy.cnblogs.com。

(本文完)

退出移动版