相关文章推荐
寂寞的眼镜  ·  百鬼封尽漫画免费 - 百鬼封尽漫画 - ...·  2 年前    · 
爱看书的小狗  ·  似曾相识的魔术-魔术王子别撩我-漫画牛·  2 年前    · 
Code  ›  深入 Go 中各个高性能 JSON 解析库开发者社区
string fastjson使用 结构体类型 fastjson
https://cloud.tencent.com/developer/article/1827699
销魂的钥匙扣
3 月前
luozhiyun

深入 Go 中各个高性能 JSON 解析库

原创
腾讯云
开发者社区
文档 建议反馈 控制台
首页
学习
活动
专区
圈层
工具
MCP广场
文章/答案/技术大牛
发布
首页
学习
活动
专区
圈层
工具
MCP广场
返回腾讯云官网
luozhiyun
首页
学习
活动
专区
圈层
工具
MCP广场
返回腾讯云官网
社区首页 > 专栏 > 深入 Go 中各个高性能 JSON 解析库

深入 Go 中各个高性能 JSON 解析库

原创
作者头像
luozhiyun
修改 于 2021-05-22 22:17:13
修改 于 2021-05-22 22:17:13
4.6K 0
举报
文章被收录于专栏: luozhiyun的技术学习 luozhiyun的技术学习

转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.luozhiyun.com/archives/535

其实本来我是没打算去看 JSON 库的性能问题的,但是最近我对我的项目做了一次 pprof,从下面的火焰图中可以发现在业务逻辑处理中,有一半多的性能消耗都是在 JSON 解析过程中,所以就有了这篇文章。

image-20210519160937326
image-20210519160937326

这篇文章深入源码分析一下在 Go 中标准库是如何解析 JSON 的,然后再看看有哪些比较流行的 Json 解析库,以及这些库都有什么特点,在什么场景下能更好的帮助我们进行开发。

主要介绍分析以下几个库:

库名

Star

标准库 JSON Unmarshal

valyala/fastjson

1.2 k

tidwall/gjson

8.3 k

buger/jsonparser

4 k

json-iterator 库也是一个非常有名的库,但是我测了一下性能和标准库相差很小,相比之下还是标准库更值得使用;

Jeffail/gabs 库与 bitly/go-simplejson 直接用的标准库的 Unmarshal 来进行解析,所以性能上和标准库一致,本篇文章也不会提及;

easyjson 这个库需要像 protobuf 一样为每一个结构体生成序列化的代码,具有强入侵性,我个人不是很喜欢,所以也没提及。

上面的这些库是我能搜到的 Star 数大于 1k 比较知名,并且仍然在迭代的 JSON 解析库,如果有遗漏的,可以联系我,我会补上。

标准库 JSON Unmarshal

分析

代码语言: txt
复制
func Unmarshal(data []byte, v interface{})

官方的 JSON 解析库需要传两个参数,一个是需要被序列化的对象,另一个是表示这个对象的类型。

在真正执行 JSON 解析之前会调用 reflect.ValueOf 来获取参数 v 的反射对象。然后会获取到传入的 data 对象的开头非空字符来界定该用哪种方式来进行解析。

代码语言: txt
复制
func (d *decodeState) value(v reflect.Value) error {
	switch d.opcode {
	default:
		panic(phasePanicMsg)
	// 数组 
	case scanBeginArray:
	// 结构体或map
	case scanBeginObject:
	// 字面量,包括 int、string、float 等
	case scanBeginLiteral:
	return nil
}

如果被解析的对象是以 [ 开头,那么表示这是个数组对象会进入到 scanBeginArray 分支;如果是以 { 开头,表明被解析的对象是一个结构体或 map,那么进入到 scanBeginObject 分支 等等。

以解析对象为例:

代码语言: txt
复制
func (d *decodeState) object(v reflect.Value) error {
	var fields structFields
	// 检验这个对象的类型是 map 还是 结构体
	switch v.Kind() {
	case reflect.Map: 
	case reflect.Struct:
		// 缓存结构体的字段到 fields 对象中
		fields = cachedTypeFields(t)
		// ok
	default:
		d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)})
		d.skip()
		return nil
	var mapElem reflect.Value
	origErrorContext := d.errorContext
	// 循环一个个解析JSON字符串中的 key value 值
	for {  
		start := d.readIndex()
		d.rescanLiteral()
		item := d.data[start:d.readIndex()]
		// 获取 key 值
		key, ok := unquoteBytes(item)
		if !ok {
			panic(phasePanicMsg)
		var subv reflect.Value
		destring := false   
		// 根据 value 的类型反射设置 value 值 
		if destring {
			// value 值是字面量会进入到这里
			switch qv := d.valueQuoted().(type) {
			case nil:
				if err := d.literalStore(nullLiteral, subv, false); err != nil {
					return err
			case string:
				if err := d.literalStore([]byte(qv), subv, true); err != nil {
					return err
			default:
				d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type()))
		} else {
			// 数组或对象会递归调用 value 方法
			if err := d.value(subv); err != nil {
				return err
		// 直到遇到 } 最后退出循环
		if d.opcode == scanEndObject {
			break
		if d.opcode != scanObjectValue {
			panic(phasePanicMsg)
	return nil
}
  1. 首先会缓存结构体对象;
  2. 循环遍历结构体对象;
  3. 找到结构体中的 key 值之后再找到结构体中同名字段类型;
  4. 递归调用 value 方法反射设置结构体对应的值;
  5. 直到遍历到 JSON 中结尾 } 结束循环。

小结

通过看 Unmarshal 源码中可以看到其中使用了大量的反射来获取字段值,如果是多层嵌套的 JSON 的话,那么还需要递归进行反射获取值,可想而知性能是非常差的了。

但是如果对性能不是那么看重的话,直接使用它其实是一个非常好的选择,功能完善的同时并且官方也一直在迭代优化,说不定在以后的版本中性能也会得到质的飞跃。

fastjson

库地址: https://github.com/valyala/fastjson

这个库的特点和它的名字一样就是快,它的介绍页是这么说的:

Fast. As usual, up to 15x faster than the standard encoding/json.

它的使用也是非常的简单,如下:

代码语言: txt
复制
func main() {
	var p fastjson.Parser
	v, _ := p.Parse(`{
                "str": "bar",
                "int": 123,
                "float": 1.23,
                "bool": true,
                "arr": [1, "foo", {}]
	fmt.Printf("foo=%s\n", v.GetStringBytes("str"))
	fmt.Printf("int=%d\n", v.GetInt("int"))
	fmt.Printf("float=%f\n", v.GetFloat64("float"))
	fmt.Printf("bool=%v\n", v.GetBool("bool"))
	fmt.Printf("arr.1=%s\n", v.GetStringBytes("arr", "1"))
// Output:
// foo=bar
// int=123
// float=1.230000
// bool=true
// arr.1=foo

使用 fastjson 首先要将被解析的 JSON 串交给 Parser 解析器进行解析,然后通过 Parse 方法返回的对象来获取。如果是嵌套对象可以直接在 Get 方法传参的时候传入相应的父子 key 即可。

分析

fastjson 在设计上和标准库 Unmarshal 不同的是,它将 JSON 解析划分为两部分:Parse、Get。

Parse 负责将 JSON 串解析成为一个结构体并返回,然后通过返回的结构体来获取数据。在 Parse 解析的过程是无锁的,所以如果想要在并发地调用 Parse 进行解析需要使用 ParserPool

fastjson 是从上往下依次遍历 JSON ,然后解析好的数据存放在 Value 结构体中:

代码语言: txt
复制
type Value struct {
	o Object
	a []*Value
	s string
	t Type
}

这个结构体非常简成:

  • o Object :表示被解析的结构是一个对象;
  • a []*Value :表示表示被解析的结构是个数组;
  • s string :如果被解析的结构不是对象也不是数组,那么其他类型的值会以字符串的形式存放在这个字段中;
  • t Type :表示这个结构的类型,有 TypeObject、TypeArray、TypeString、TypeNumber等。
代码语言: txt
复制
type Object struct {
	kvs           []kv
	keysUnescaped bool
type kv struct {
	k string
	v *Value
}

这个结构存放对象的递归结构。如果把上面例子中的 JSON 串解析完毕之后就是这样一个结构:

fastjson
fastjson

代码

在代码实现上,由于没有了反射部分的代码,所以整个解析过程变得非常的清爽。我们直接看看主干部分的解析:

代码语言: txt
复制
func parseValue(s string, c *cache, depth int) (*Value, string, error) {
	if len(s) == 0 {
		return nil, s, fmt.Errorf("cannot parse empty string")
	depth++
	// 最大深度的json串不能超过MaxDepth
	if depth > MaxDepth {
		return nil, s, fmt.Errorf("too big depth for the nested JSON; it exceeds %d", MaxDepth)
	// 解析对象
	if s[0] == '{' {
		v, tail, err := parseObject(s[1:], c, depth)
		if err != nil {
			return nil, tail, fmt.Errorf("cannot parse object: %s", err)
		return v, tail, nil
	// 解析数组
	if s[0] == '[' {
	// 解析字符串
	if s[0] == '"' {
	return v, tail, nil
}

parseValue 会根据字符串的第一个非空字符来判断要解析的类型。这里用一个对象类型来做解析:

代码语言: txt
复制
func parseObject(s string, c *cache, depth int) (*Value, string, error) {
	o := c.getValue()
	o.t = TypeObject
	o.o.reset()
	for {
		var err error
		// 获取Ojbect结构体中的 kv 对象
		kv := o.o.getKV()
		// 解析 key 值
		kv.k, s, err = parseRawKey(s[1:])
		// 递归解析 value 值
		kv.v, s, err = parseValue(s, c, depth)
		// 遇到 ,号继续往下解析
		if s[0] == ',' {
			s = s[1:]
			continue
		// 解析完毕
		if s[0] == '}' {
			return o, s[1:], nil
		return nil, s, fmt.Errorf("missing ',' after object value")
}

parseObject 函数也非常简单,在循环体中会获取 key 值,然后调用 parseValue 递归解析 value 值,从上往下依次解析 JSON 对象,直到最后遇到 } 退出。

小结

通过上面的分析可以知道 fastjson 在实现上比标准库简单不少,性能也高上不少。使用 Parse 解析好 JSON 树之后可以多次反复使用,避免了需要反复解析进而提升性能。

但是它的功能是非常的简陋的,没有常用的如 JSON 转 Struct 或 JSON 转 map 的操作。如果只是想简单的获取 JSON 中的值,那么使用这个库是非常方便的,但是如果想要把 JSON 值转化成一个结构体就需要自己动手一个个设值了。

GJSON

库地址: https://github.com/tidwall/gjson

GJSON 在我的测试中,虽然性能是没有 fastjson 这么极致,但是功能是非常完善,性能也是相当 OK 的,下面先简单介绍一下 GJSON 的功能。

GJSON 的使用是和 fastjson 差不多的,也是非常的简单,只要在参数中传入 json 串以及需要获取的值即可:

代码语言: txt
复制
json := `{"name":{"first":"li","last":"dj"},"age":18}`
lastName := gjson.Get(json, "name.last")

除了这个功能以外还可以进行简单的模糊匹配,支持在键中包含通配符 * 和 ? , * 匹配任意多个字符, ? 匹配单个字符,如下:

代码语言: txt
复制
json := `{
	"name":{"first":"Tom", "last": "Anderson"},
	"age": 37,
	"children": ["Sara", "Alex", "Jack"]
fmt.Println("third child*:", gjson.Get(json, "child*.2"))
fmt.Println("first c?ild:", gjson.Get(json, "c?ildren.0"))
  • child*.2 :首先 child* 匹配 children , .2 读取第 3 个元素;
  • c?ildren.0 : c?ildren 匹配到 children , .0 读取第一个元素;

除了模糊匹配以外还支持修饰符操作:

代码语言: txt
复制
json := `{
	"name":{"first":"Tom", "last": "Anderson"},
	"age": 37,
	"children": ["Sara", "Alex", "Jack"]
fmt.Println("third child*:", gjson.Get(json, "children|@reverse"))

children|@reverse 先读取数组 children ,然后使用修饰符 @reverse 翻转之后返回,输出。

代码语言: txt
复制
nestedJSON := `{"nested": ["one", "two", ["three", "four"]]}`
fmt.Println(gjson.Get(nestedJSON, "nested|@flatten"))

@flatten 将数组 nested 的内层数组平坦到外层后返回:

代码语言: txt
复制
["one","two","three", "four"]

等等还有一些其他有意思的功能,大家可以去查阅一下官方文档。

分析

GJSON 的 Get 方法参数是由两部分组成,一个是 JSON 串,另一个叫做 Path 表示需要获取的 JSON 值的匹配路径。

在 GJSON 中因为要满足很多的定义的解析场景,所以解析是分为两部分的,需要先解析好 Path 之后才去遍历解析 JSON 串。

在解析过程中如果遇到可以匹配上的值,那么会直接返回,不需要继续往下遍历,如果是匹配多个值,那么会一直遍历完整个 JSON 串。如果遇到某个 Path 在 JSON 串中匹配不到,那么也是需要遍历完整个 JSON 串。

在解析的过程中也不会像 fastjson 一样将解析的内容保存在一个结构体中,可以反复的利用。所以当调用 GetMany 想要返回多个值的时候,其实也是需要遍历 JSON 串多次,因此效率会比较低。

GJSON
GJSON

除此之外,在解析 JSON 的时候并不会对它进行校验,即使这个放入的字符串不是个 JSON 也会照样解析,所以需要用户自己去确保放入的是 JSON 。

代码

代码语言: txt
复制
func Get(json, path string) Result {
	// 解析 path 
	if len(path) > 1 {
	var i int
	var c = &parseContext{json: json}
	if len(path) >= 2 && path[0] == '.' && path[1] == '.' {
		c.lines = true
		parseArray(c, 0, path[2:])
	} else {
		// 根据不同的对象进行解析,这里会一直循环,直到找到 '{' 或 '['
		for ; i < len(c.json); i++ {
			if c.json[i] == '{' {
				parseObject(c, i, path)
				break
			if c.json[i] == '[' {
				parseArray(c, i, path)
				break
	if c.piped {
		res := c.value.Get(c.pipe)
		res.Index = 0
		return res
	fillIndex(json, c)
	return c.value
}

Get 方法里面可以看到有很长一串的代码是用来解析各种 Path,然后一个 for 循环一直遍历 JSON 直到找到 '{' 或 '[',然后才进行相应的逻辑进行处理。

代码语言: txt
复制
func parseObject(c *parseContext, i int, path string) (int, bool) {
	var pmatch, kesc, vesc, ok, hit bool
	var key, val string
	rp := parseObjectPath(path)
	if !rp.more && rp.piped {
		c.pipe = rp.pipe
		c.piped = true
	// 嵌套两个 for 循环 寻找 key 值
	for i < len(c.json) {
		for ; i < len(c.json); i++ {
			if c.json[i] == '"' { 
				var s = i
				for ; i < len(c.json); i++ {
					if c.json[i] > '\\' {
						continue
					// 找到 key 值跳转到 parse_key_string_done
					if c.json[i] == '"' {
						i, key, kesc, ok = i+1, c.json[s:i], false, true
						goto parse_key_string_done
				key, kesc, ok = c.json[s:], false, false
			// 直接break
			parse_key_string_done:
				break
			if c.json[i] == '}' {
				return i + 1, false
		if !ok {
			return i, false
		// 校验是否是模糊匹配
		if rp.wild {
			if kesc {
				pmatch = match.Match(unescape(key), rp.part)
			} else {
				pmatch = match.Match(key, rp.part)
		} else {
			if kesc {
				pmatch = rp.part == unescape(key)
			} else {
				pmatch = rp.part == key
		// 解析 value
		hit = pmatch && !rp.more
		for ; i < len(c.json); i++ {
			switch c.json[i] {
			default:
				continue
			case '"':
				i, val, vesc, ok = parseString(c.json, i)
				if !ok {
					return i, false
				if hit {
					if vesc {
						c.value.Str = unescape(val[1 : len(val)-1])
					} else {
						c.value.Str = val[1 : len(val)-1]
					c.value.Raw = val
					c.value.Type = String
					return i, true
			case '{':
				if pmatch && !hit {
					i, hit = parseObject(c, i+1, rp.path)
					if hit {
						return i, true
				} else {
					i, val = parseSquash(c.json, i)
					if hit {
						c.value.Raw = val
						c.value.Type = JSON
						return i, true
 
推荐文章
寂寞的眼镜  ·  百鬼封尽漫画免费 - 百鬼封尽漫画 - 漫画在线全集免费阅读 - 腾讯动漫
2 年前
爱看书的小狗  ·  似曾相识的魔术-魔术王子别撩我-漫画牛
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号