基准测试
3 种类型少量数据中等数据大量数据
jsonparser
少量数据
每个测试将 190 字节的 http 日志转换为 JSON。
Library | time/op | bytes/op | allocs/op |
---|---|---|---|
encoding/json struct | 7879 | 880 | 18 |
encoding/json interface{} | 8946 | 1521 | 38 |
buger/jsonparser | 1367 | 0 | 0 |
buger/jsonparser (EachKey API) | 809 | 0 | 0 |
jsonparser≈ 10
中等数据
每个测试处理一个 2.4kb 的 JSON 记录,读取多个嵌套字段和 1 个数组。
Library | time/op | bytes/op | allocs/op |
---|---|---|---|
encoding/json struct | 57749 | 1336 | 29 |
encoding/json interface{} | 79297 | 10627 | 215 |
buger/jsonparser | 15955 | 0 | 0 |
buger/jsonparser (EachKey API) | 8916 | 0 | 0 |
jsonparser≈ 6.5
大量数据
每个测试处理一个 24kb 的 JSON 记录,读取 2 个数组,并获取数组中每个元素的一些字段。
Library | time/op | bytes/op | allocs/op |
---|---|---|---|
encoding/json struct | 748336 | 8272 | 307 |
encoding/json interface{} | 1224271 | 215425 | 3395 |
buger/jsonparser | 85308 | 0 | 0 |
jsonparser≈ 9
和标准库的差异
jsonparserJSON
方法对应关系
标准库 | jsonparser | |
---|---|---|
编码 | Marshal | Set |
解码 | Unmarshal | Get |
jsonparserGetIntGetString
package main
import (
"fmt"
"log"
"github.com/buger/jsonparser"
)
var (
// JSON 字符串
dataJson = []byte(`
{
"person": {
"name": {
"first": "Leonid",
"last": "Bugaev",
"fullName": "Leonid Bugaev"
},
"github": {
"handle": "buger",
"followers": 109
},
"avatars": [
{ "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" }
]
},
"company": {
"name": "Acme"
}
}
`)
)
func main() {
// 解析对象
github := struct {
Handle string `json:"handle"`
Followers int `json:"followers"`
}{}
err := jsonparser.ObjectEach(dataJson, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
switch string(key) {
case "handle":
github.Handle = string(value)
case "followers":
followers, _ := jsonparser.ParseInt(value)
github.Followers = int(followers)
}
return nil
}, "person", "github")
if err != nil {
log.Fatal(err)
}
fmt.Printf("github = %+v\n\n", github)
// 编码结构体
githubJson, err := jsonparser.Set([]byte(`{}`), []byte(fmt.Sprintf(`{"handle: %s", "followers": "%d"}`, github.Handle, github.Followers)), "github")
if err != nil {
log.Fatal(err)
}
fmt.Printf("github json = %s\n\n", githubJson)
// 解析多个 key
paths := [][]string{
{"person", "name", "fullName"},
{"person", "avatars", "[0]", "url"},
{"company", "name"},
}
jsonparser.EachKey(dataJson, func(i int, bytes []byte, valueType jsonparser.ValueType, err error) {
switch i {
case 0:
fmt.Printf("fullName = %s\n", bytes)
case 1:
fmt.Printf("avatars[0].url = %s\n", bytes)
case 2:
fmt.Printf("company.name = %s\n\n", bytes)
}
}, paths...)
// 解析整数
n, err := jsonparser.GetInt(dataJson, "person", "github", "followers")
if err != nil {
log.Fatal(err)
}
fmt.Printf("n = %d\n\n", n)
// 解析字符串
name, err := jsonparser.GetString(dataJson, "company", "name")
if err != nil {
log.Fatal(err)
}
fmt.Printf("name = %s\n", name)
}
$ go run main.go
# 输出如下
github = {Handle:buger Followers:109}
github json = {"github":{"handle: buger", "followers": "109"}}
fullName = Leonid Bugaev
avatars[0].url = https://avatars1.githubusercontent.com/u/14009?v=3&s=460
company.name = Acme
n = 109
name = Acme
jsonparser解码编码Set
代码实现
go1.19 linux/amd64jsonparserv1.1.1
jsonparserparser.go解码 / Get编码 / Set
数据类型
jsonparser
// JSON 数据类型常量
type ValueType int
const (
NotExist = ValueType(iota)
String
Number
Object
Array
Boolean
Null
Unknown
)
GC[]byte堆上GC[]byte堆上
// 规避 GC 的切片容量上限常量
const unescapeStackBufSize = 64
辅助方法
stringEnd"\"
func stringEnd(data []byte) (int, bool) {}
blockEnd[]{}
func blockEnd(data []byte, openSym byte, closeSym byte) int {}
lastToken[' ', '\n', '\r', '\t']
func lastToken(data []byte) int {}
nextToken[' ', '\n', '\r', '\t']
func nextToken(data []byte) int {}
tokenEndtoken[' ', '\n', '\r', '\t', ',', '}', ']']token
func tokenEnd(data []byte) int {}
findTokenStarttoken
func findTokenStart(data []byte, token byte) int {}
findKeyStartkey
func findKeyStart(data []byte, key string) (int, error) {}
getTypeJSON 数据类型常量
func getType(data []byte, offset int) ([]byte, ValueType, int, error) {}
searchKeys 状态机
该方法的内部代码较长,这里就不用具体的文字描述流程了,直接将重点的代码部分用注释进行标记。
func searchKeys(data []byte, keys ...string) int {
keyLevel := 0 // 当前查找 key 的 层级
level := 0 // 当前遍历层级
i := 0 // 当前遍历字符索引
ln := len(data) // 参数 data 长度
lk := len(keys) // 参数 keys 的层级,如示例代码中的 person.name.fullName
lastMatched := true
if lk == 0 {
// 如果 keys 参数的层级为 0,直接返回
return 0
}
var stackbuf [unescapeStackBufSize]byte // 长度为 64 byte 切片
for i < ln {
// 从左向右, 逐个 byte 遍历
switch data[i] {
case '"':
// 遍历到 " 字符
i++
// 将当前位置设置为 key 的开始位置
keyBegin := i
strEnd, keyEscaped := stringEnd(data[i:])
if strEnd == -1 {
// 如果没有找到对应的结尾 " 字符,直接返回
return -1
}
// 将下次遍历字符位置更新为: 结尾 " 字符的下一个字符位置
i += strEnd
keyEnd := i - 1
// 查找结尾 " 字符的下一个 token
// 正常的情况下应该是一个冒号 :
// 例如 `"fullName": "Leonid Bugaev"` 中 "fullName" 后面跟着的 : 字符
valueOffset := nextToken(data[i:])
if valueOffset == -1 {
// 如果没有直接返回
return -1
}
// 将下次遍历字符位置更新为: 下一个 token 的位置
i += valueOffset
if data[i] == ':' {
// 如果下一个 token 正好是一个冒号 :
// 说明两个 " 之间的字符串是一个 key
if level < 1 {
// 如果当前层级为 0, 说明参数 JSON 字符串格式是错的,直接返回
return -1
}
// 将字符串 key 提取出来
key := data[keyBegin:keyEnd]
// 处理 key 字符串转义的情况
var keyUnesc []byte
if !keyEscaped {
keyUnesc = key
} else if ku, err := Unescape(key, stackbuf[:]); err != nil {
return -1
} else {
keyUnesc = ku
}
if level <= len(keys) {
if equalStr(&keyUnesc, keys[level-1]) {
// 如果当前字符串和参数 keys 当前查找元素相同
// 那么标记参数 keys 当前查找元素已经找到
lastMatched = true
// 如果当前层级减 1 等于当前查找 key 层级
if keyLevel == level-1 {
// 当前查找 key 层级加 1 (说明又找到了一个 key)
keyLevel++
// 如果当前查找 key 层级等于参数 keys 的层级
// 说明参数中的所有 key 已经全部找到,直接返回即可
if keyLevel == lk {
return i + 1
}
}
} else {
// 如果当前字符串和查到的 keys 第一个元素不相同
// 那么标记参数 keys 当前查找元素未找到
// 接下来继续从参数 keys 的第一个元素开始查找
lastMatched = false
}
} else {
// 如果当前层级比参数 keys 层级还要大
// 说明没有找到对应的 keys, 直接返回
return -1
}
} else {
// 如果下一个 token 不是一个冒号 :
// 说明两个 " 之间的字符串不是一个 key
// 那么就从当前位置继续向后遍历
i--
}
case '{':
if !lastMatched {
// 如果父级 keys 未匹配,此时应该匹配 key (也就是应该以 " 开头)
// 所以直接跳过当前的对象块 {} 即可
end := blockEnd(data[i:], '{', '}')
if end == -1 {
return -1
}
i += end - 1
} else {
// 如果父级 keys 已经匹配,将当前遍历层级加 1
level++
}
case '}':
// 如果遇到 } 字符,将当前层级变量减 1 即可
// 如果当前层级 和 当前查找 key 的 层级相同,将 keyLevel 层级减 1
// 说明当前 key 的所有子 keys 不可能在这个对象中找到了
level--
if level == keyLevel {
keyLevel--
}
case '[':
// 如果当前层级 和 当前查找 key 的 层级相同
// 并且当前查找 key 的类型是数组
if keyLevel == level && keys[level][0] == '[' {
var keyLen = len(keys[level])
...
// 通过 ArrayEach 遍历数组元素
ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
...
})
if valueFound == nil {
// 如果没有当前 key, 说明数组中不存在对应的 key 元素
// 没有必要继续查找了,直接返回
return -1
} else {
// 如果数组中存在对应的 key 元素
// 递归从该元素中查找参数 keys 剩余的部分
subIndex := searchKeys(valueFound, keys[level+1:]...)
if subIndex < 0 {
return -1
}
return i + valueOffset + subIndex
}
} else {
// 如果当前层级 和 当前查找 key 的 层级不相同
// 或者当前查找 key 的类型不是数组
// 直接跳过当前数组 [] 数据块接口
if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 {
return -1
} else {
i += arraySkip - 1
}
}
case ':':
// 边界情况处理,正常的 JSON key 中不应该包含 :
// 直接返回 -1 表示没有找到对应的 keys
return -1
}
i++ // 索引 + 1, 遍历下个字符
}
return -1
}
searchKeys解析操作有限状态机
for
状态机组成部分
searchKeys[]bytesearchKeyssearchKeyskeyLevellevellnlk
状态转移表
状态字符 | 初始 | 对象开始 | 对象结束 | key 开始 | key 结束 | 数组开始 | 数组结束 | 比较 key | 跳过当前数据块 | 停止解析 |
---|---|---|---|---|---|---|---|---|---|---|
{ | 对象开始 | 停止解析 | 停止解析 | 跳过当前数据块 | 对象开始 | |||||
} | 停止解析 | 对象结束 | 停止解析 | 对象结束 | ||||||
" | 停止解析 | key 开始 | key 结束 | 停止解析 | ||||||
: | 停止解析 | 停止解析 | 停止解析 | 停止解析 | 停止解析 | 停止解析 | 停止解析 | |||
[ | 停止解析 | 数组开始 | 比较 key | 跳过当前数据块 |
状态转移表状态转移表
Get 方法
searchKeys 状态机辅助方法Get
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
a, b, _, d, e := internalGet(data, keys...)
return a, b, d, e
}
GetinternalGet
func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) {
// 第一步
if len(keys) > 0 {
if offset = searchKeys(data, keys...); offset == -1 {
return nil, NotExist, -1, -1, KeyPathNotFoundError
}
}
// 第二步
nO := nextToken(data[offset:])
if nO == -1 {
return nil, NotExist, offset, -1, MalformedJsonError
}
// 第三步
offset += nO
value, dataType, endOffset, err = getType(data, offset)
if err != nil {
return value, dataType, offset, endOffset, err
}
// 第四步
if dataType == String {
value = value[1 : len(value)-1]
}
return value[:len(value):len(value)], dataType, offset, endOffset, nil
}
[]bytesearchKeyskeyssearchKeystoken两个位置getTypekeyJSON 数据类型常量getTypekey"
Set 和 Delete 方法
GetSetDelete有限状态机
其他工具类函数
编码/解码
[]byte[]bytestringstring[]byterfc7159
高性能原理
JSON反射interface{}[]byte有限状态机[]byte[]byte编码/解码
小结
jsonparser避免反射[]byte 参数引用基于状态机编码/解码操作jsonparser应用层代价
最后顺便提一句,字节跳动开源了他们基于汇编进行开发 JOSN 组件库 sonic, 将 JSON 操作性能又提升到了一个新的高度。