前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang原生json可以一库走天下吗?

Golang原生json可以一库走天下吗?

作者头像
腾讯云开发者
发布2021-10-12 12:11:17
2.6K0
发布2021-10-12 12:11:17
举报

导语 | Go的“玩家”们看到这个题目可能会很疑惑——对于JSON而言,Go原生库encoding/json已经是提供了足够舒适的JSON处理工具,广受Go开发者的好评。它还能有什么问题?Golang原生json可以一库走天下吗?实际上在业务开发过程中,我们遇到了不少原生json做不好甚至是做不到的问题,还真是不能完全满足我们的要求。那么,它有什么问题吗?什么情况下使用第三方库?如何选型?性能如何?

一、部分常用的GO JSON解析库

(一)Go原生encoding/json

这应该是广大Go程序员最熟悉的库了,使用json.Unmarshal和json.Marshal函数,可以轻松将JSON格式的二进制数据反序列化到指定的Go结构体中,以及将Go结构体序列化为二进制流。而对于未知结构或不确定结构的数据,则支持将二进制反序列化到map[string]interface{} 类型中,使用KV的模式进行数据的存取。

这里我提两个大家可能不会留意到的额外特性:

  • json包解析的是一个JSON数据,而JSON数据既可以是对象(object),也可以是数组(array),同时也可以是字符串(string)、数值(number)、布尔值(boolean)以及空值(null)。而上述的两个函数,其实也是支持对这些类型值的解析的。比如下面段代码也是能用的:
代码语言:javascript
复制
var s stringerr := json.Unmarshal([]byte(`"Hello, world!"`), &s)// 注意字符串中的双引号不能缺,如果仅仅是 `Hello, world`,则这不是一个合法的JSON序列,会返回错误。
  • json在解析时,如果遇到大小写问题,会尽可能地进行大小写转换。即便是一个key与结构体中的定义不同,但如果忽略大小写后是相同的,那么依然能够为字段赋值。比如下面的例子可以说明:
代码语言:javascript
复制
cert := struct {    Username string `json:"username"`    Password string `json:"password"`}{}    err := json.Unmarshal([]byte(`{"UserName":"root","passWord":"123456"}`), &cert)if err != nil {    fmt.Println("err =", err)} else {    fmt.Println("username =", cert.Username)    fmt.Println("password =", cert.Password)}// 实际输出: // username = root// password = 123456

(二)jsoniter

打开jsoniter的GitHub主页,它一上来就宣扬两个关键词:high-performance以及compatible。这也是这个包最大的两个卖点。

首先说兼容:jsoniter最大的优势就在于:能够100%兼容标准库,因此代码能够非常方便地进行迁移。实在是不方便的。也可以用Go Monkey强行换掉json的相关函数入口。

接着看性能:与其他吹嘘自己性能的开源库一样,它们自己的测试结论都不能无脑采信。这里我根据我个人的测试情况,先抛几个简单的结论吧:

  • 在反序列化结构体这一单一场景下,jsoniter相比标准库确实有提升,我自己测得的结果大约是提升1.4倍左右。
  • 但同样是反序列化结构体这单一场景,jsoniter远远不如easyjson。
  • 其他场景则不见得,后面我会再做说明。

从性能上,jsoniter能够比众多大神联合开发的官方库性能还快的主要原因,一个是尽量减少不必要的内存复制,另一个是减少reflect的使用——同一类型的对象,jsoniter只调用reflect解析一次之后即缓存下来。不过随着go版本的迭代,原生json库的性能也越来越高,jsonter的性能优势也越来越窄。

此外,jsoniter还支持Get函数,支持直接从一个[]byte二进制数据中读取响应的字段,这个后文再做说明。

(三)easyjson

这是GitHub上面的另一个JSON解析包。相比起jsoniter多达9k的star而言,easyjson似乎少一点,有3k,但其实也算是一个人气很高的开源项目。

这个包最主要的卖点,依然是快。为什么easyjson比jsoniter还要快?因为easyjson的开发模式与protobuf类似,在程序运行之前需要使用其代码工具,为每一个结构体专门生成序列化/反序列化的程序代码。每一个程序都有定制化的解析函数。

但也因为这种开发模式,easyjson对业务的侵入性比较高。一方面,在go build之前需要先生成代码;另一方面,相关的JSON处理函数也不兼容原生json库。

(四)jsonparser

这是我个人非常喜欢的一个JSON解析库,3.9k的star数也可以看出它人气不低。它的GitHub主页标题就号称比官方库有高达10x的性能。

还是那句话:开源项目自己的测试结论都不能无脑采信。这个10x的性能我个人也测出来过,但不能代表所有的场景。

为什么jsonparser有那么高的性能呢?因为对于jsonparser本身,它只负责解构出一个二进制字节串中的一些关键边界字符,比如说:

  • 找到“,那么就找到结束的”,这中间就是一个字符串。
  • 找到[,那么就找到成对的],这中间就是一个数组。
  • 找到{,那么就找到成对的},这中间就是一个对象。
  • ……

然后,它将找到的数据中间的这段[]byte数据交给调用方,由调用方进行进一步的处理。此时,对这些二进制数据的解析和合法性检查是需要调用方来负责的。

为什么看起来这么麻烦的开源库我会喜欢呢?因为开发者可以基于jsonparser,构建特殊逻辑,甚至是构建自己的json解析库。我自己的开源项目jsonvalue在早期也基于 jsonparser 实现,尽管后来为了进一步优化性能而弃用了jsonparser,但这不影响我对它的推崇。

(五)jsonvalue

这个项目是个人的JSON解析库,设计之初是为了替代原生JSON库使用map[string]interface{}来处理非结构化JSON数据的需求。为此我有另外一篇文章叙述了这个问题:《还在用map[string]interface{}处理 JSON?告诉你一个更高效的方法——jsonvalue》。

我大致完成了对库的优化(参见最新版本),性能已经远远高于原生json库,并且略微优于jsoniter。当然,这也是特定情况下的,针对各种大相径庭的场景,各种库性能各不相同。

二、常规操作下的JSON处理

除了struct和map之外,还有别的?下面就我在实际业务开发中遇到的场景都列一下,以飨读者。所有测试代码均开源,读者可以查阅,也可以向我提出意见,提issue、评论、私聊均可。

(一)常规操作:结构体解析

结构体解析,这是Go中处理JSON最最常规的操作了。这里我定义了这样的一个结构体:

代码语言:javascript
复制
type object struct {  Int    int       `json:"int"`  Float  float64   `json:"float"`  String string    `json:"string"`  Object *object   `json:"object,omitempty"`  Array  []*object `json:"array,omitempty"`}

稍微使了点坏——这个结构可以疯狂自嵌套。

然后呢,我再定义了一段二进制流,用json.cn可以看到,这是一个有5层结构的JSON对象。

代码语言:javascript
复制
{"int":123456,"float":123.456789,"string":"Hello, world!","object":{"int":123456,"float":123.456789,"string":"Hello, world!","object":{"int":123456,"float":123.456789,"string":"Hello, world!","object":{"int":123456,"float":123.456789,"string":"Hello, world!","object":{"int":123456,"float":123.456789,"string":"Hello, world!"},"array":[{"int":123456,"float":123.456789,"string":"Hello, world!"},{"int":123456,"float":123.456789,"string":"Hello, world!"}]}}},"array":[{"int":123456,"float":123.456789,"string":"Hello, world!"},{"int":123456,"float":123.456789,"string":"Hello, world!"}]}

使用这两个结构,分别对官方encoding/json,jsoniter。easyjson三个包进行Marshal和Unmarshal的测试。首先我们看反序列化(Unmarshal)的测试结果:

下面是序列化的测试结果:

纯粹从性能上来看,easyjson不愧是专门为每一个struct定制化了序列化和反序列化函数,它达到了最高的性能,比另外两个库均有2.5~3倍的效率。而jsoniter略高于官方json,不过相差不大。

(二)常规的非常规操作: map[string]interface{}

说是 “非常规” 的原因是,在这种情况下,程序需要处理非结构化的JSON 数据,或者是在一段函数中处理多种不同类型的数据结构,因而不能使用结构体模式来处理。官方JSON库的解决方案是(对于对象类型)采用 map[string]interface{}来保存。这个场景下,只有官方json和jsoniter支持。

测试数据如下,首先是反序列化:

序列化情况测试数据如下:

可见,针对这种情况,大家都是半斤八两,jsoniter并没有什么明显优势。即便是jsoniter作为卖点的大数据量解析,优势也是微乎其微。在同等数据量情况下,两个库反序列化的耗时基本上是结构体情况的两倍,而序列化时间则是结构体情况的约2.5倍。

老铁们能不用这种操作就不要用了吧,更何况程序在处理interface{}时还需要各种断言,这种痛苦,各位可以看我的文章感受一下。

三、非常规操作——反序列化篇

真的涉及到无法使用struct的时候,就各种开源项目八仙过海各显神通了。每一个库其实都有非常详细和强大的额外功能,单单本文肯定无法说完。这里我就几个库及其代表的思路列举一下吧,后面也会附上各种情况的测试数据。

(一)jsoniter

在处理非结构化JSON中,如果要解析一段[]byte数据并获得其中的某个值,jsoniter有以下相类似的方案。

第一种方案是直接解析原文并返回所需数据:

代码语言:javascript
复制
// 读取二进制数据中 response.userList 数组中的第一个元素的 name 字段username := jsoniter.Get(data, "response", "userList", 0, "name")fmt.Println("username:", username.ToString())

也可以是直接返回一个对象,并且基于该对象可以继续操作:

代码语言:javascript
复制
obj := jsoniter.Get(data)if obj.ValueType() == jsoniter.InvalidType {    // err handling}username := obj.Get("response", "userList", 0, "name")fmt.Println("username:", username.ToString())

这个函数有一个非常大的特点,那就是按需解析。比如说在这个语句obj :=jsoniter.Get(data) 中,jsoniter就只做了最低限度的数据检查,至少先解析出了当前是一个object类型的JSON,其他部分的解析均不做。

而即便是到了第二个调用obj.Get(“response”,“userList”,0,“name”) 中,jsoniter也是竭尽可能减少不必要的解析,只解析需要解析的部分。

比如说,请求参数中要求解析response.userList的值,那么jsoniter在遇到诸如response.gameList等无关字段的时候,那么jsoniter就回尽量绕开而不去处理,从而尽可能地减少无关的CPU时间。

不过需要注意的是,返回的这个obj对象,从接口功能来看,可以理解为它是只读的,无法重新序列化为二进制序列。

(二)jsonparser

相对于jsoniter,要解析一段[]byte数据并获得其中的某个值,jsonparser的支持比较有限。

比如说,如果我们能够实现知道某一个值的类型,比如说上面的username字段,那么我们可以这么获取:

代码语言:javascript
复制
username, err := jsonparser.GetString(data, "response", "userList", "[0]", "name")if err != nil {    // err handling}fmt.Println("username:", username)

但是jsonparser的Get系列函数只能获得除了null之外的基本类型,也就是number, boolean, string三种。

如果要操作object和array,就要熟悉下面的两个函数,而这两个函数我个人觉得就是jsonparser的核心:

代码语言:javascript
复制
func ArrayEach(    data []byte,     cb func(value []byte, dataType ValueType, offset int, err error),     keys ...string,) (offset int, err error)
func ObjectEach(    data []byte,     callback func(key []byte, value []byte, dataType ValueType, offset int) error,     keys ...string,) (err error)

这两个函数按顺序解析二进制数据,并将提取出的数据段通过回调函数返回给调用方,由调用方对数据进行操作。调用方可以组map,可以组slice,甚至可以做一些平常无法操作的操作(后文会做说明)

(三)jsonvalue

这个是我本人开发的开源Go JSON操作库,在Get类操作的API设计风格上与jsoniter的第二种风格比较类似。

比如我们同样是要获取前面所说的username字段,那么我们可以这么获取:

代码语言:javascript
复制
v, err := jsonvalue.Unmarshal(data)if err != nil {    // err handling}username := v.GetString("response", "userList", 0, "name")fmt.Println("username:", username)

(四)性能测试对比

在本小节所说的“非常规操作”场景下,三个库中,jsoniter和jsonparser在解析时都是“按需解析”,而jsonvalue则是全面解析。因此在制定测试方案的时候还是有所区别。

这里我先抛出测试数据,测试评价中有两部分:

  • 性能评价: 表示在该场景下的性能评分,不考虑是否好用,仅考虑CPU执行效率高不高。
  • 功能评价: 表示在该场景下,获得数据之后,程序后续的处理是否方便。不考虑反序列化性能高不高。

上述测试数据中将反序列化场景中分为四种。这里我详细说明一下四种情况的应用场景,以及相应的技术选型建议:

浅解析

在测试代码中,浅解析指的是针对一个较深层级的结构,仅仅将其最浅一层的key列表解析出来。这个场景更多的是作为参考。可以看到,jsonparser的性能完爆其他开源库,它可以以最快的速度将第一层的key列表解析出来。

但是在易用性方便,jsonparser和jsoniter都需要开发者对获得的数据再做进一步的处理,因此jsoniter和jsonparser的易用性在这个场景下均略低。

获取正文中的具体某一个数据

这个场景是这样的:JSON数据正文中,仅有一小部分数据对当前业务有用并需要获取。这里我分了两种情况:

  • 有用数据占全部数据的比例较高(对应“读取其中一个层级较深的数据”)
  • 这一场景下从性能上来看,jsonparser的表现一如既往地优越。
  • 从易用性的角度,jsonparser需要调用方再行处理一遍数据,因此jsoniter和jsonvalue更胜一筹。
  • 有用数据占全部数据的比例较低(对应“从大量(100x)数据中仅读取其中一个较深层级的值”)
  • 这一场景从性能上看,jsonprser依然完爆。
  • 从易用性的角度,jsonparser依然很弱。
  • 综合易用性和性能,这一场景中,有用数据的比例越低,jsonparser的价值就越高。
  • 业务需要完整解析数据——这一场景是对各个方案综合性能最完整的考量
  • jsonvalue完成全部且完整的解析,其耗时比号称高速的jsoniter更低。
  • 相比jsonparser,虽然jsonvalue表面上的处理时间是jsonparser的2.5倍,但后者只是完成了数据的半加工,而前者则是将成品拿了出来给调用方使用。
  • 从性能的角度,jsonparser依然优秀,但在这一场景中,易用性其实很成问题——复杂的遍历操作之中,还需要再封装逻辑去存储数据。
  • 性能第二是jsonvalue,这也是笔者非常自信的地方。
  • 至于jsoniter,在这个场景下就不要用了——在需要对数据全解析的情况下,它的数据简直没法看。
  • 最后还加上了官方json库和jsoniter解析map的数据,仅作参考——在这个场景下,也建议是不要用了。

四、非常规操作——序列化篇

这里指的是在没有结构体的情况下,序列化一段数据。这种场景一般发生在以下情况中:

  • 需要序列化的数据格式不确定,可能会根据其他的参数来生成。
  • 需要序列化的数据过多且过于琐碎,如果一一定义结构体并进行marshal的话,代码的可读性太差。

这个场景的第一个解决方案就是前文提到的“常规的非常规操作”,也就是使用map

至于非常规操作嘛,我们首先排除jsoniter和jsonparser,因为他们没有直接的构建自定义json结构的方法。接着排除easyjson,因为它无法针对map操作。剩下的也就只有jsonvalue了。

比如我们返回用户的昵称,假设返回格式是:{“code”:0,“message”:“success”,“data”:{“nickname”:“振兴中华”}}。

使用map的代码如下:

代码语言:javascript
复制
code := 0nickname := "振兴中华"
res := map[string]interface{}{    "code": code,    "message": "success",    "data": map[string]string{        "nickname": nickname    },}b, _ := json.Marshal(&res)

而jsonvalue的方法为:

代码语言:javascript
复制
res := jsonvalue.NewObject()res.SetInt(0).At("code")res.SetString("success").At("message")res.SetString(nickname).At("data", "nickname")b := res.MustMarshal()

应该说易用性上,都非常方便。我们针对官方json、jsoniter、jsonvalue分别进行序列化操作,测得的数据如下:

结果已经非常明显了。这个原因大家也能明白,因为在处理map的时候,需要使用reflect机制去处理数据类型,这大大降低了程序的性能。

五、结论以及选型建议

(一)结构体序列化和反序列化

在这个场景中,我个人首推的是官方的json库。可能读者会比较意外。以下是我的观点:

  • 虽然easyjson的性能压倒其他所有开源项目,但它有一个最大的缺陷,那就是需要额外使用工具来生成这段代码,而对这额外工具的版本控制就多了一分运维成本。当然如果读者的团队已经能够很好地处理protobuf了的话,那么也是可以用同样的思路来管理easyjson的。
  • Go1.8之前,官方json库的性能就收到多方诟病。但现今(1.16.3)官方json库的性能已不可同日而语。此外,作为使用最为广泛(没有之一)的json库,官方库的bug是最少的、兼容性也是最好的。
  • jsoniter的性能虽然依然优于官方,但没有达到逆天的程度,如果希望有极致的性能,那么你应该选择easyjson而不是jsoniter。
  • jsoniter近年已经不活跃了,笔者前段时间提了一个issue没人回复。后来在上去看了一下issue列表,发现居然还遗留一些2018年的issue。

(二)非结构化数据的序列化和反序列化

这个场景下,我们要分高数据利用率和低数据利用率两种情况来看。所谓数据利用率,指的是JSON数据的正文中,如果说超过四分之一的数据都是业务需要关注和处理的,那就算是高数据利用率。

  1. 高数据利用率-这种情况下,我推荐使用jsonvalue。
  2. 低数据利用率-这里分两种情况:JSON数据是否还需要重新序列化回去。
    • 无需重新序列化:这个时候,选择jsonparser就行了,它的性能实在是耀眼。
    • 需要重新序列化:这种情况,有两种选择,如果对性能要求相对较低,可以使用jsonvalue;如果性能的要求要求高,并且只需要往二进制序列中仅仅插入一个数据(重要),那么可以采用jsoniter的Set方法。读者可以查阅godoc。

实际操作中,超大JSON数据量、同时需要重新序列化的情况非常少。这种场景下往往是是代理服务器、网关、overlay中继服务等,同时又需要往原数据中注入额外信息的时候使用。换句话说,jsoniter的适用场景比较有限。

下面是从10%到60%数据覆盖率下,不同库的操作效率对比(纵坐标单位:μs/op)

可以看到,当jsoniter的数据利用率达到25%时,相比jsonvalue就已经没有任何优势;而jsonparser则是40%左右。

六、其他“邪道”操作

笔者在实际应用中还遇到过一些关于JSON奇奇怪怪的处理场景,也趁这个机会列出来,分享一下我的解决方案。

(一)不区分大小写的JSON

前文说到:“json在解析时,如果遇到大小写问题,会尽可能地进行大小写转换。即便是一个key与结构体中的定义不同,但如果忽略大小写后是相同的,那么依然能够为字段赋值。”

但是呢,如果你使用的是map、jsoniter、jsonparser,这就是个大问题了。我们有两个服务,同时操作MySQL数据库中的同一个字段,但是两个Go服务所定义的结构体中,有一个字母的大小写不一致。这个问题是长期存在的,但因为官方json解析结构体时的上述特性,导致这个问题一直没有暴露。直到有一天,我们写了一个脚本程序洗数据的时候,采用了map方式来读取这个字段的时候,Bug就曝光了。

于是我后来把大小写支持的特性加入了jsonvalue中,解决了这个问题:

代码语言:javascript
复制
raw := `{"user":{"nickName":"pony"}}` // 注意当中的 Nv, _ := jsonvalue.UnmarshalString(raw)
fmt.Println("nickname:", v.GetString("user", "nickname"))fmt.Println("nickname:", v.Caseless().GetString("user", "nickname"))
// 输出// nickname:// nickname: pony

(二)有顺序的JSON对象

在合作兄弟模块的接口时,对方推数据流的时候是以一个JSON对象的格式给到我们的业务模块中的。后来根据需求,推过来的数据要求是有序的。如果接口格式改成数组,那么就需要对双方接口的数据结构作较大的改动。此外,我们在滚动升级的时候势必遇到新旧模块同时存在的情况,所以接口需要同时兼容两套接口格式。

最后我们采用了一个很“邪道”的方式——数据生产方是能够按顺序将KV推出来的,而我们作为消费方,使用jsonparser的ObjectEach函数,就能够按顺序获得kv字节序列,从而也完成数据的顺序获取。

(三)跨语言UTF-8字符串对接

Go算是一个很年轻的语言,在它诞生的时候,互联网上的主流字符编码已经是unicode,编码格式则是UTF-8了。而其他辈分更老的语言,由于各种原因,可能采用了不一样的编码格式。

这就导致了在进行跨语言JSON对接时,不同团队、不同公司针对unicode宽字符时,采用的编码格式可能不同。如果遇到这种情况,那么解决方法就是统一采用ASCII编码。如果是官方json,可以将宽字符转义成ascii。

如果是使用jsonvalue,则默认就是ascii转义,比如说:

代码语言:javascript
复制
v := jsonvalue.NewObject()v.SetString("中国").At("nation")fmt.Println(v.MustMarshalString())
// 输出// {"nation":"\u4E2D\u56FD"

( 转载需取得作者同意,未经许可,禁止二次转载 )

参考资料:

1.本文中涉及到的开源库:jsoniter、rapidjson、jsonparser、easyjson、jsonvalue、Go monkeypatching

2.本文涉及的测试数据和测试方法参见:jsonvalue-test

3.JSON序列化中的转义和Unicode编码

4.号称全世界最快的JSON解析器,比别的快10x

5.json-iterator/go使用笔记

6.如何评价jsoniter自称是最快的JSON解析器

7.JSON-ITERATOR使用要注意的大坑

8.Go学习28使用easyjson高效解析json数据

 作者简介

张敏

腾讯高级后台工程师

腾讯高级后台工程师,在电子和互联网行业深耕多年,拥有丰富的嵌入式和云服务后台开发经验,个人博客共有过百篇文章,腾讯云开发者社区Top50原创作者,技术创作101第二季讲师,现负责腾讯产品后台开发。

 推荐阅读

这次全了,8种超详细Web跨域解决方案!

10分钟带你玩转Kafka基于Controller的领导选举!

LLVM极简教程:9个步骤!实现一个简单编译器

避坑指南!手把手带你解读html2canvas的实现原理


👇戳「阅读原文」前往「腾讯腾讯云开发者社区」作者个人主页参与交流哦~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • (一)Go原生encoding/json
  • 这应该是广大Go程序员最熟悉的库了,使用json.Unmarshal和json.Marshal函数,可以轻松将JSON格式的二进制数据反序列化到指定的Go结构体中,以及将Go结构体序列化为二进制流。而对于未知结构或不确定结构的数据,则支持将二进制反序列化到map[string]interface{} 类型中,使用KV的模式进行数据的存取。
  • (二)jsoniter
  • (三)easyjson
  • (四)jsonparser
  • (五)jsonvalue
  • (一)常规操作:结构体解析
  • (二)常规的非常规操作: map[string]interface{}
  • (一)jsoniter
  • (二)jsonparser
  • (三)jsonvalue
  • (四)性能测试对比
    • 浅解析
      • 获取正文中的具体某一个数据
      • 这里指的是在没有结构体的情况下,序列化一段数据。这种场景一般发生在以下情况中:
        • (一)结构体序列化和反序列化
          • (二)非结构化数据的序列化和反序列化
            • (一)不区分大小写的JSON
              • (二)有顺序的JSON对象
                • (三)跨语言UTF-8字符串对接
                • 参考资料:
                相关产品与服务
                文件存储
                文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档