我们可选择的 json 库有很多,而之所以放弃官方的 json 库,多是出于性能考虑。不过,在选择采用某个 json 库之前,建议大家不要盲目相信性能评分,还是要深入自己业务场景,多出一些自己 benchmark 的评测结果。
最重要的,还是要搞清楚,选择的 json 库究竟做了哪些优化,才使得性能有了较大提高,它的这些优化项是否符合我们的业务场景?
主要分析下面2个json库,判断他们的性能差异
① github.com/valyala/fastjson
② github.com/bitly/go-simplejson
假设我们要解码的字符串都是二维数组类型,业务需求是读取二维数组中的某个元素。二维数组的example如下,这样的编码方式主要为了节省存储空间,省去了存储 key 的额外开销。我们要读取内容中的 china 值,也就是第一个数组中的第二个元素:
[["2", "china", "10", "55", "1", "text", "0", "0", "0", "11ms", "cpu"],[""]]
使用 fastjson 读取 china 的代码实现:
func FastJson(str string) string {
json, _ := fastjson.Parse(str)
arrs, _ := json.Array()
for _, elem := range arrs {
sub, _ := elem.Array()
result, _ := sub[1].StringBytes()
return string(result)
}
return ""
}
使用simplejson读取china 的代码实现:
func SimpleJson(str string) string {
json, _ := simplejson.NewJson([]byte(str))
arrs, _ := json.Array()
for _, elem := range arrs {
sub, _ := elem.([]interface{})
return sub[1].(string)
}
return ""
}
编写benchmark测试用例,分析两个方法的性能,测试用例如下:
import "testing"
var (
str = `[["2", "china", "10", "55", "1", "text", "0", "0", "0", "11ms", "cpu"],[""]]`
china string
)
func BenchmarkFastJson(b *testing.B) {
for i := 0; i < b.N; i++ {
china = FastJson(str)
}
}
func BenchmarkSimpleJson(b *testing.B) {
for i := 0; i < b.N; i++ {
china = SimpleJson(str)
}
}
每个方法执行5次,控制台查看benchmark的分析结果,粗略统计,fastjson是simplejson性能的2倍。通过 benchstat 我们还可以获得性能的分析结果
go test -bench=. -benchmem -count=5 . | tee p.txt

在使用 benchstat之前需要先安装这个工具,它分析的数据源就是我们 bench 执行的输出结果,全部导出到了文件 p.txt中。
go get -u golang.org/x/perf/cmd/benchstat
使用 benchstat 来查看性能的分析结果
benchstat p.txt
通过分析结果,我们可以清楚的看到两个函数的性能差异,单次执行的耗时、申请内存次数、内存大小的比较

fastjson文档说,比原生的json快了 15x 的性能,这还远远没有达到呢。其实fastjson提供了sync.Pool的功能,我们在FastJson方法的基础上应用对象池来看看性能的变化。
另外,我们要比较fastjson使用sync.Pool和不使用sync.Pool的两种方式。观察一下两者的性能差异。先将当前的go test 执行程序保存下来
# 该例子中不需要保持,删除这段
# go test -c
# 重命名 *.test 为 old.text
修改代码,fastjson使用sync.Pool的处理方式,将FastJson修改为如下,通过ParserPool声明一个对象池,然后每次执行解析的时候从池中获取一个parser对象,执行完成之后,放回去
var fastjsonPool = fastjson.ParserPool{}
func FastJson(str string) string {
parser := fastjsonPool.Get()
defer fastjsonPool.Put(parser)
json, _ := parser.Parse(str)
arrs, _ := json.Array()
for _, elem := range arrs {
sub, _ := elem.Array()
result, _ := sub[1].StringBytes()
return string(result)
}
return ""
}
我们将这次benchmark的执行结果保存到new.txt中,重新执行benchmark
go test -bench=. -benchmem -count=5 . | tee new.txt
通过 benchstat 比较两次的性能差异
benchstat p.txt new.txt
我们看FastJson old 和 new 的比较,性能提升了6倍

字节 sonic 库给出了非常高的性能评分,针对 json 的性能优化,我们又多了一个选择。sonic 基上完全兼容了官方的 json 库,所以无需担心兼容问题。
有一点需要指出:使用 sonic 在 marshal map 类型数据时,编码的顺序是不确定的。因为 map 的读取本身也是不保证顺序的,每次读取key/value 顺序可能都不相同。但神奇的是,官方json库的 map 序列化编码顺序是固定的,会按照 ASCII 码的升序排列。
用下面的单测来验证一下,使用 sonic 编码后的 map 字段顺序是不固定的,而使用 json 编码后的 map 编码顺序是固定的。虽然 map 类型的编码顺序不那么重要,但还是挺奇怪的。
// github.com/bytedance/sonic v1.9.2
func TestSonicMap(t *testing.T) {
var meta = map[string]string{
"a": "a",
"b": "b",
"c": "c",
"d": "d",
}
a, _ := sonic.Marshal(meta)
b, _ := sonic.Marshal(meta)
t.Log(string(a), string(b))
aj, _ := json.Marshal(meta)
bj, _ := json.Marshal(meta)
t.Log(string(aj), string(bj))
}
原来是官方的 json 库内部做了排序处理,可以查看 mapEncoder 的编码实现。源码中使用 sort.Slice 函数对 map 的键值做了升序处理。
// Extract and sort the keys.
sv := make([]reflectWithString, v.Len())
mi := v.MapRange()
for i := 0; mi.Next(); i++ {
sv[i].k = mi.Key()
sv[i].v = mi.Value()
if err := sv[i].resolve(); err != nil {
e.error(fmt.Errorf("json: encoding error for type %q: %q", v.Type().String(), err.Error()))
}
}
sort.Slice(sv, func(i, j int) bool { return sv[i].ks < sv[j].ks })
在看 snoic 用法的过程中,还看到了 反序列化结构中的字符串是不安全的 #191 Issue,挺有意思,不过我本地验证了一下,Issue 已经被修复了。关于 string 和 []byte 类型的强制内存转换,是性能和安全的权衡问题。