本文介绍的是 jsonvalue 库,这是我个人在 Github 上开发的第一个功能比较多而全的 Go 库。目前主要是在腾讯未来社区的开发中使用,用于取代 map[string]interface{}
。
为什么开发这个库?
Go 是后台开发的新锐。Go 工程师们早期就会接触到 "encoding/json"
库:对于已知格式的 JSON 数据,Go 的典型方法是定义一个 struct
来序列化和反序列化 (marshal/unmarshal
)。
但是对于未知格式,亦或者是不方便固定格式的情形,典型的解决方法是采用 map[string]interface{}
来处理。但是在实际应用中,这个方案是存在一些不足的。
map[string]interface{} 存在的不足
有一些情况下,我们确实需要采用 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
}
不知道读者是什么感觉,反正我是要掀桌了……
Marshal() 效率较低
在 Unmarshal()
中,map[string]interface{}
类型的反序列化效率比 struct
略低一点,但大致相当。但在 Marshal()
的时候,两者的差别就非常明显了。根据后文的一个测试方案,map
的耗时是 struct
的五倍左右。一个序列化/反序列化操作下来,就要多耗费一倍的时间。
jsonvalue 功能介绍
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,则自动判断当前层级是否为 Json object,如果不是,则返回 error
- 解析道某一层时,如果当前参数类型为整型数字,则自动判断当前层级是否为 Json array,如果不是,则返回 error
- 从 array 中取值时,如果给定的数组下标超出 array 长度,则返回 error
- 从 object 中取值时,如果制定的 key 不存在,则返回 error
- 最终获取到制定的键值对,则会判断一下类型是否为 Json string,是的话返回 string 值,否则返回 error
也就是说,在前面的问题中一长串的检查,都在这个函数中自动帮你解决了。
除了 string 类型外,jsonvalue
也支持 GetBool, GetNull, GetInt, GetUint, GetInt64, GetArray, GetObject
等等一系列的类型获取,只要你想到的 Json 类型都提供。
JSON 编辑
大部分情况下,我们需要编辑一个 JSON object。使用 j := jsonvalue.NewObject()
。后续可以采用 SetXxx().At()
系列函数设置子成员。与前面所说的 GetXxx
系列函数一样,其实 jsonvalue 也支持一站式的复杂结构生成。下面我们一个一个说明:
设置 JSON object 的子成员
比如在 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 array 的子成员
为 JSON 数组添加子成员也是必要的功能。同样地,我们先创建一个数组:a := jsonvalue.NewArray()
。对数组的基本操作有以下几个:
// 在数组的开头添加元素
a.AppendString("Hello, world!").InTheBegging()
// 在数组的末尾添加元素
a.AppendInt(5678).InTheEnd()
// 在数组中指定位置的前面插入元素
a.InsertFloat32(3.14159).Before(1)
// 在数组中指定位置的后面插入元素
a.InsertNull().After(2)
快速编辑 JSON 更深层级的内容
针对编辑场景,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 值。具体如下:
- 允许传入不定数的参数,依次往下解析
- 解析到某一层时,如果下一层参数类型为 string,则自动判断当前层级是否为 Json object,如果不是,则返回 error
- 解析道某一层时,如果下一层参数类型为整型数字,则自动判断当前层级是否为 Json array,如果不是,则返回 error
- 解析到某一层时,如果没有后续参数了,那么这就是最终目标,则按照前面的
SetXxxx
所指定的子成员类型,创建子成员
具体到上面的例子,那么整个操作逻辑如下:
SetString()
函数表示准备设置一个 string 类型的子成员At()
函数表示开始在 JSON 对象中寻址。"Response"
参数,首先检查到这不是最后一个参数,那么首先判断当前的j
是不是一个 object 对象,如果不是,则返回 error- 如果
"Response"
对象存在,则取出;如不存在,则创建,然后内部递归地调用response.SetString("Hello, world!").At("Result", "AnArray", 0, "SomeString")
"Result"
同理- 拿到
"Result"
层的对象之后,检查下一个参数,发现是整型,则函数判断为预期下一层目标"AnArray"
应该是一个数组。那么函数内首先获取这个目标,如果不存在,则创建一个数组;如果存在,则如果该目标不是数组的话,会返回 error 拿到
"AnArray"
之后,当前参数为整数。这里的逻辑比较复杂:- 如果该参数等于 -1,则表示在当前数组的末尾添加元素
- 如果该参数的值等于当前数组的长度,也表示在当前数组的末尾添加元素
- 如果该参数的值大于等于零,且小于当前数组的长度,则表示将当前数组的指定位置替换为新的指定元素
- 最后一个参数
"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 性能测试
我对 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 的其他高级参数
除了上述基本操作之外,jsonvalue 在序列化时还支持一些 map 方案所无法实现的功能。笔者过段时间再把这些内容另文记录吧。读者也可以参照 jsonvalue 的 godoc,文档中有详细说明。
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原作者: amc,欢迎转载,但请注明出处。
原文标题:还在用 map[string]interface{} 处理 JSON?告诉你一个更高效的方法——jsonvalue
发布日期:2020-08-10
原文发布于云+社区,也是本人的博客
有疑问加站长微信联系(非本文作者)