本文介绍的是 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
原文公布于云 + 社区,也是自己的博客