本文介绍的是 jsonvalue 库,这是我我的在 Github 上开发的第一个功能比较多而全的 Go 库。目前主要是在腾讯将来社区的开发中使用,用于取代 map[string]interface{}
。git
Go 是后台开发的新锐。Go 工程师们早期就会接触到 "encoding/json"
库:对于已知格式的 JSON 数据,Go 的典型方法是定义一个 struct
来序列化和反序列化 (marshal/unmarshal
)。github
可是对于未知格式,亦或者是不方便固定格式的情形,典型的解决方法是采用 map[string]interface{}
来处理。可是在实际应用中,这个方案是存在一些不足的。json
有一些状况下,咱们确实须要采用 map[string]interface{}
来解析并处理 JSON,这每每出如今中间件、网关、代理服务器等等须要处理所有或部分格式未知的 JSON 逻辑中。数组
假设我有一个 unmarshal 以后的 map: m := map[string]interface{}{}
,当我要判断一个键值对(如 "aNum"
)是否是数字时,须要分别判断两种状况:服务器
v, exist := m["aNum"] if false == exist { return errors.New("aNum does not exist") } n, ok := v.(float64) if false == ok { return fmt.Errorf("'%v' is not a number", v) }
好比腾讯云 API,其数据返回格式嵌套几层,示意以下:函数
{ "Response": { "Result": { "//": "这里我假设须要查找下面这个字段:", "AnArray": [ { "SomeString": "Hello, world!" } ] } } }
当接口出错的时候,会返回:性能
{ "Response": { "Error": { "Code": "error code", "Message": "error message" } } }
假设在正常逻辑中,咱们由于一些因素,必须使用 map[string]interface{}
来解析数据。难么当须要判断 Response.Result.AnArray[0].SomeString
的值时,因为咱们不能100%信任对端的数据(可能服务器被劫持了、崩溃了、被入侵了等等可能),而须要对各个字段进行检查,于是完整的代码以下:测试
m := map[string]interface{}{} // 一些 unmarshal 动做 // ...... // // 首先要判断接口是否错误 var response map[string]interface{} var ok bool // // 首先要获取 Response 信息 if v, exist := m["Response"]; !exist { return errors.New("missing Response") // // 而后须要判断 Response 是否是一个 object 类型 } else if response, ok = v.(map[string]interface{}); !ok { return errors.New("Response is not an object") // // 而后须要判断是否有 Error 字段 } else if e, exist = response["Error"]; exist { return fmt.Errorf("API returns error: %_+v", e) } // // 而后才判断具体的值 // 首先,还须要判断是否有 Result 字段 if resultV, exist := response["Result"]; !exist { return errors.New("missing Response.Result") // // 而后再判断 Result 字段是否 object } else if result, ok := resultV.(map[string]interface{}); !ok { return errors.New("Response.Result is not an object") // // 而后再获取 AnArray 字段 } else if arrV, exist := resule["AnArray"]; !exist { return errors.New("missing Response.Result.AnArray") // // 而后再判断 AnArray 的类型 } else if arr, ok := arrV.([]interface{}); !ok { return errors.New("Response.Result.AnArray is not an array") // 而后再判断 AnArray 的长度 } else if len(arr) < 1 { return errors.New("Response.Result.AnArray is empty") // // 而后再获取 array 的第一个成员,而且判断是否为 object } else if firstObj, ok := arr[0].(map[string]interface{}); !ok { return errors.New("Response.Result.AnArray[0] is not an object") // // 而后再获取 SomeString 字段 } else if v, exist := firstObj["SomeString"]; !exist { return errors.New("missing Response.Result.AnArray[0].SomeString") // // 而后再判断 SomeString 的类型 } else if str, ok := v.(string); !ok { return errors.New("Response.Result.AnArray[0].SomeString is not a string") // // 终于完成了!!! } else { fmt.Printf("SomeString = '%s'\n", str) return nil }
不知道读者是什么感受,反正我是要掀桌了……jsonp
在 Unmarshal()
中,map[string]interface{}
类型的反序列化效率比 struct
略低一点,但大体至关。但在 Marshal()
的时候,二者的差异就很是明显了。根据后文的一个测试方案,map
的耗时是 struct
的五倍左右。一个序列化/反序列化操做下来,就要多耗费一倍的时间。spa
Jsonvalue 是一个用于处理 JSON 的 Go 语言库。其中解析 json 文本的部分基于 jsonparser 实现。而解析具体内容、JSON 的 CURD、序列化工做则独立实现。
首先咱们介绍一下基本的使用方法
Jsonvalue 也提供了响应的 marshal/unmarshal 接口来序列化/反序列化 JSON 串。咱们之前面获取 Response.Result.AnArray[0].SomeString
的功能举例说明,包含完整错误检查的代码以下:
// 反序列化 j, err := jsonvalue.Unmarshal(plainText) if err != nil { return err } // 判断接口是否返回了错误 if e, _ := jsonvalue.Get("Response", "Error"); e != nil { return fmt.Errorf("Got error from server: %v", e) } // 获取咱们要的字符串 str, err := j.GetString("Response", "Result", "AnArray", 0, "SomeString") if err != nil { return err } fmt.Printf("SomeString = '%s'\n", str) return nil
结束了。是否是很简单?在 j.GetString(...)
中,函数完成了如下几个功能:
也就是说,在前面的问题中一长串的检查,都在这个函数中自动帮你解决了。
除了 string 类型外,jsonvalue
也支持 GetBool, GetNull, GetInt, GetUint, GetInt64, GetArray, GetObject
等等一系列的类型获取,只要你想到的 Json 类型都提供。
大部分状况下,咱们须要编辑一个 JSON object。使用 j := jsonvalue.NewObject()
。后续能够采用 SetXxx().At()
系列函数设置子成员。与前面所说的 GetXxx
系列函数同样,其实 jsonvalue 也支持一站式的复杂结构生成。下面咱们一个一个说明:
好比在 j
下设置一个 string 类型的子成员:someString = 'Hello, world!'
j.SetString("Hello, world!").At("someString") // 表示 “在 'someString' 键设置 string 类型值 'Hello, world!'”
一样地,咱们也能够设置其余的类型:
j.SetBool(true).At("someBool") // "someBool": true j.SetArray().At("anArray") // "anArray": [] j.SetInt(12345).At("anInt") // "anInt": 12345
为 JSON 数组添加子成员也是必要的功能。一样地,咱们先建立一个数组:a := jsonvalue.NewArray()
。对数组的基本操做有如下几个:
// 在数组的开头添加元素 a.AppendString("Hello, world!").InTheBegging() // 在数组的末尾添加元素 a.AppendInt(5678).InTheEnd() // 在数组中指定位置的前面插入元素 a.InsertFloat32(3.14159).Before(1) // 在数组中指定位置的后面插入元素 a.InsertNull().After(2)
针对编辑场景,jsonvalue 也提供了快速建立层级的功能。好比咱们前文提到的 JSON:
{ "Response": { "Result": { "AnArray": [ { "SomeString": "Hello, world!" } ] } } }
使用 jsonvalue 只须要两行就能够生成一个 jsonvalue 类型对象(*jsonvalue.V
):
j := jsonvalue.NewObject() j.SetString("Hello, world!").At("Response", "Result", "AnArray", 0, "SomeString")
在 At()
函数中,jsonvalue 会递归地检查当前层级的 JSON 值,而且按照参数的要求,若有必要,自动地建立相应的 JSON 值。具体以下:
SetXxxx
所指定的子成员类型,建立子成员具体到上面的例子,那么整个操做逻辑以下:
SetString()
函数表示准备设置一个 string 类型的子成员At()
函数表示开始在 JSON 对象中寻址。"Response"
参数,首先检查到这不是最后一个参数,那么首先判断当前的 j
是否是一个 object 对象,若是不是,则返回 error"Response"
对象存在,则取出;如不存在,则建立,而后内部递归地调用 response.SetString("Hello, world!").At("Result", "AnArray", 0, "SomeString")
"Result"
同理"Result"
层的对象以后,检查下一个参数,发现是整型,则函数判断为预期下一层目标 "AnArray"
应该是一个数组。那么函数内首先获取这个目标,若是不存在,则建立一个数组;若是存在,则若是该目标不是数组的话,会返回 error拿到 "AnArray"
以后,当前参数为整数。这里的逻辑比较复杂:
"SomeString"
是一个 string 类型,那么表示 AnArray[0]
应是一个 object,则在 AnArray[0]
位置建立一个 JSON object,而且设置 {"SomeString":"Hello, world!"}
其实能够看到,上面的流程对于目标为数组类型来讲,不太直观。所以对于目标 JSON 为数组的层级,前文提到的 Append
和 Insert
函数也支持不定量参数。举个例子,若是咱们须要在上述说起的 Response.Result.AnArray
数组末尾添加一个 true
的话,能够这么调用:
j.AppendBool(true).InTheEnd("Response", "Result", "AnArray")
将一个 jsonvalue.V
序列化的方式也很简单:b, _ := j.Marshal()
便可以生成 []byte
类型的二进制串。只要正常使用 jsonvalue
,是不会产生 error 的,所以能够直接采用 b := j.MustMarshal()
对于须要直接得到 string 类型的序列化结果的状况,则使用 s := j.MustMarshalString()
,因为内部是使用 bytes.Buffer
直接输出,能够减小 string(b)
转换带来的额外耗时。
我对 jsonvalue
、预约义的 struct
、map[string]interface{}
三种模式进行了对比,简单地将整型、浮点、字符串、数组、对象集中类型混搭和嵌套,测试结果以下:
Unmarshal
操做对比
数据类型 | 循环次数 | 每循环耗时 | 每循环内存占用 | 每循环 allocs 数 |
---|---|---|---|---|
map[string]interface{} |
1000000 | 11357 ns | 4632 字节 | 132 次 |
struct |
1000000 | 10966 ns | 1536 字节 | 49 次 |
jsonvalue |
1000000 | 10711 ns | 7760 字节 | 113 次 |
Marshal
操做对比
数据类型 | 循环次数 | 每循环耗时 | 每循环内存占用 | 每循环 allocs 数 |
---|---|---|---|---|
map[string]interface{} |
806126 | 15028 ns | 5937 字节 | 121 次 |
struct |
3910363 | 3089 ns | 640 字节 | 1 次 |
jsonvalue |
2902911 | 4115 ns | 2224 字节 | 5 次 |
能够看到,jsonvalue 在反序列化的效率比 struct 和 map 方案均略强一点;在序列化上,struct 和 jsonvalue 远远将 map 方案抛在身后,其中 jsonvalue 耗时比 struct 多出约 1/3。综合来看,jsonvalue 的反序列化+序列化耗时比 struct 多出 5.5% 左右。毕竟 jsonvalue 处理的是不肯定格式的 Json,这个成绩其实已经比较能够了。
上文所述的测试命令为 go test -bench=. -run=none -benchmem -benchtime=10s
,CPU 为第十代 i5 2GHz。
读者能够参见个人 benchmark 文件。
除了上述基本操做以外,jsonvalue 在序列化时还支持一些 map 方案所没法实现的功能。笔者过段时间再把这些内容另文记录吧。读者也能够参照 jsonvalue 的 godoc,文档中有详细说明。
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原做者: amc,欢迎转载,但请注明出处。
原文标题:还在用 map[string]interface{} 处理 JSON?告诉你一个更高效的方法——jsonvalue
发布日期:2020-08-10
原文发布于云+社区,也是本人的博客