Go 语言操作 JSON 的几种方法

标准库的 json 模块

Go 语言标准库 encoding/json
提供了操作 JSON 的方法,一般可以使用 json.Marshal
json.Unmarshal
来序列化和解析 JSON 字符串:

// 定义结构体
type User struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

// 序列化
buf, err := json.Marshal(User{
    Email:    "xxxx@example.com",
    Password: "123456",
})

// 解析
user := User {}
err := json.Unmarshal([]byte(`{
  "email": "xxx@example.com",
  "password": "123456"
}`), &user)

更加灵活和更好性能的 jsoniter 模块

标准库 encoding/json
在使用时需要预先定义结构体,使用时显得不够灵活。这时候可以尝试使用 github.com/json-iterator/go
模块,其除了 提供与标准库一样的接口之外,还提供了一系列更加灵活的操作方法

val := []byte(`{"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}`)

// 仅解析 Colors 字段,并直接得到 string 类型
str := jsoniter.Get(val, "Colors", 0).ToString()

另辟蹊径提高性能的 easyjson 模块

标准库 encoding/json
需要依赖反射来实现,因此性能上会比较差。 github.com/mailru/easyjson
则是利用 go generate
机制自动为结构体生成实现了 MarshalJSON
UnmarshalJSON
方法的代码,在序列化和解析时可以直接生成对应字段的 JSON 数据,而不需要运行时反射。据官方的介绍,其性能是标准库的 4~5 倍,是其他 json 模块的 2~3 倍。

要使用 easyjson
模块,首先执行以下命令安装 easyjson
命令:

go get -u github.com/mailru/easyjson/...

然后新建文件 school.go
,并定义结构体 School

//easyjson:json
type School struct {
    Name string     `json:"name"`
    Addr string     `json:"addr"`
}

接着执行 easyjson -all school.go
,此时目录下会生成一个新的文件 school_easyjson.go
,为 School
结构体实现了 MarshalJSON
UnmarshalerJSON
方法,接着使用 easyjson 对应的方法去对这个结构体进行解析即可。

简单性能测试结果

对于以上介绍的三个模块,我测试了对于以下 JSON 字符串其序列化和解析性能的测试结果。
测试程序:

package awesomeProject

import (
    "encoding/json"
    "testing"

    jsoniter "github.com/json-iterator/go"
    "github.com/mailru/easyjson"
)

type T1 struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Intro string `json:"intro"`
    Valid bool   `json:"valid"`
}

// easyjson:json
type T2 struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Intro string `json:"intro"`
    Valid bool   `json:"valid"`
}

var d1 = T1{Name: "如何快速提升", Age: 12320, Intro: "如何快速提升 Go 程序性能?如何快速提升 Go 程序性能?如何快速提升 Go 程序性能?Well, easyJson is 4 times faster than normal json(as per its documets) ,in our organization we have used it extensively and yes its faster. Here is a small example to get started. my current directory name is easyJson", Valid: true}
var d2 = T2{Name: "如何快速提升", Age: 12320, Intro: "如何快速提升 Go 程序性能?如何快速提升 Go 程序性能?如何快速提升 Go 程序性能?Well, easyJson is 4 times faster than normal json(as per its documets) ,in our organization we have used it extensively and yes its faster. Here is a small example to get started. my current directory name is easyJson", Valid: true}
var s1 []byte

func init() {
    buf, err := json.Marshal(&d1)
    if err != nil {
        panic(err)
    }
    s1 = buf
    println(string(s1))
}

func BenchmarkStdJsonMarshal(b *testing.B) {
    i := 0
    for i < b.N {
        i++
        _, err := json.Marshal(&d1)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkStdJsonUnmarshal(b *testing.B) {
    i := 0
    var d T1
    for i < b.N {
        i++
        err := json.Unmarshal(s1, &d)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkJsoniterMarshal(b *testing.B) {
    i := 0
    for i < b.N {
        i++
        _, err := jsoniter.Marshal(&d1)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkJsoniterUnmarshal(b *testing.B) {
    i := 0
    var d T1
    for i < b.N {
        i++
        err := jsoniter.Unmarshal(s1, &d)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkJsoniterAny(b *testing.B) {
    i := 0
    var d T1
    for i < b.N {
        i++
        a := jsoniter.Get(s1)
        d.Name = a.Get("name").ToString()
        d.Age = a.Get("age").ToInt()
        d.Intro = a.Get("intro").ToString()
        d.Valid = a.Get("valid").ToBool()
    }
}

func BenchmarkJsoniterAnyLight(b *testing.B) {
    i := 0
    var d T1
    for i < b.N {
        i++
        a := jsoniter.Get(s1)
        d.Name = a.Get("name").ToString()
    }
}

func BenchmarkEasyJsonMarshal(b *testing.B) {
    i := 0
    for i < b.N {
        i++
        _, err := easyjson.Marshal(d2)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkEasyJsonUnmarshal(b *testing.B) {
    i := 0
    var d T2
    for i < b.N {
        i++
        err := easyjson.Unmarshal(s1, &d)
        if err != nil {
            panic(err)
        }
    }
}

JSON 字符串:

{
  "name":"如何快速提升",
  "age":12320,
  "intro":"如何快速提升 Go 程序性能?如何快速提升 Go 程序性能?如何快速提升 Go 程序性能?Well, easyJson is 4 times faster than normal json(as per its documets) ,in our organization we have used it extensively and yes its faster. Here is a small example to get started. my current directory name is easyJson",
  "valid":true
}

测试结果:

  • BenchmarkStdJsonMarshal-4, 使用标准库序列化,1098 ns/op
  • BenchmarkStdJsonUnmarshal-4, 使用标准库解析,5006 ns/op
  • BenchmarkJsoniterMarshal-4, 使用 jsoniter 序列化,1106 ns/op
  • BenchmarkJsoniterUnmarshal-4, 使用 jsoniter 解析,816 ns/op
  • BenchmarkJsoniterAny-4,使用 jsoniter 的 Any 解析,4092 ns/op
  • BenchmarkJsoniterAnyLight-4,使用 jsoniter 的 Any 解析一个字段,1263 ns/op
  • BenchmarkEasyJsonMarshal-4, 使用 easyjson 序列化,1658 ns/op
  • BenchmarkEasyJsonUnmarshal-4, 使用 easyjson 解析,952 ns/op

从以上结果可以看到, jsoniter
在序列化和解析时均有比较好的性能, easyjson
次之,标准库 json
则在解析时性能比较差。当然,这并不是一个比较严格的性能测试,比如没有考虑内存分配问题以及多种不同的 JSON 结构和数据长度的测试。但是,
如果综合考虑性能和灵活性, jsoniter
可能是一个不错的选择。

参考资料