golang源码分析:jsonparser不讲武德

2023-09-06 19:15:59 浏览数 (1)

github.com/buger/jsonparser 号称比官方json解析库快10倍的json解析库,我当时就惊呆了,仔细研究源码发现,这是应试选手 文字游戏的组合:它是一个json解析库,而不是反序列化库,它不支持序列化,它把json值和go对象绑定的工作交给了用户来完成,它本质上就是一个根据json路径获取对应值的文本匹配库,所以它不需要反射和内存分配,总之太不讲武德了,下面分析下它的源码。

它最核心的api是Get函数,有两个参数,第一个data是输入原始的json串,第二个参数是变长参数,它是从根路径到目标位置整个路径上各个key组成的数组,如果json中有数组类型,它是json的数组下标。入参keys是json路径,针对多层嵌套的类型。如果没有传key,会返回最近的json对象的值。

代码语言:javascript复制
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {

返回值有4个:

1,value指向原始jsonValue的slice,如果没有找到,返回空slice

2,dataType,json的数据类型,可以是:`NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`这几个值。

3,offset 存了key value结束位置在json内部的偏移量

4,err返回不存在,或者json语法错误,如果不存在也会把dataType设置成 `dataType` to `NotExist`

当然Get函数也有变体函数实现,比如GetInt是在Get基础上ParseInt。

代码语言:javascript复制
func GetInt(data []byte, keys ...string) (val int64, err error) {
v, t, _, e := Get(data, keys...)
return ParseInt(v)

下面看下Get具体的源码实现:

代码语言:javascript复制
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
}

它调用了internalGet:如果传了keys会调用searchKeys匹配对应的字符串,否则跳过空格,返回最近的json对象,最后返回对象类型。

代码语言:javascript复制
func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) {
    if offset = searchKeys(data, keys...); offset == -1 {
    nO := nextToken(data[offset:])
    value, dataType, endOffset, err = getType(data, offset)

searchKeys本质上是一个字符串搜索的过程,它从前往后搜索,根据json的语法定义,通过匹配到的串的首字符判断是数组、对象、字符串等类型,然后进行路径的匹配,支持转义。遇到:说明key匹配完毕,就得到当前路径上的key,和传入的key进行比较,如果匹配,进入嵌套的下一层,否则定位到当前value的结尾进行下一个key的匹配。数组按照数组下标匹配,然后递归进行剩余路径的匹配。

代码语言:javascript复制
for i < ln {
switch data[i] {
case '"':
  strEnd, keyEscaped := stringEnd(data[i:])
  //支持转义,找到当前字符串的结尾位置
  valueOffset := nextToken(data[i:])
  //跳过空格
if data[i] == ':' {
  //表示当前解析到的字符串是key
  key := data[keyBegin:keyEnd]
  if level <= len(keys) {
  if equalStr(&keyUnesc, keys[level-1]) {
  lastMatched = true
   //如果key相等说明匹配到了,进入下一层key的匹配
case '{':
  level  
  //如果上一层匹配成功了,进入下一层匹配
case '}':
  level--
  //当前对象匹配完了,回到上一层匹配
case '[':
    ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
    if curIdx == aIdx {
    valueFound = value
    valueOffset = offset
    if dataType == String {
    valueOffset = valueOffset - 2
    valueFound = data[curI valueOffset : curI valueOffset len(value) 2]
    }
    }
    curIdx  = 1
    })
    //如果是数组按照数组下标匹配
    subIndex := searchKeys(valueFound, keys[level 1:]...)
    //递归进行剩余key的匹配
case ':':
    //说明json不合法
i  

这就是key匹配的核心逻辑,匹配完成后就会进行类型匹配,它的原理类似:根据前面匹配到的value的首字符,确定值的类型:

代码语言:javascript复制
func getType(data []byte, offset int) ([]byte, ValueType, int, error) {
if data[offset] == '"' {
dataType = String
} else if data[offset] == '[' { // if array value
dataType = Array
} else if data[offset] == '{' { // if object value
dataType = Object
} else {
// Number, Boolean or None
end := tokenEnd(data[endOffset:])
switch data[offset] {
case 't', 'f': // true or false
case 'u', 'n': // undefined or null
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
dataType = Number

到这里,读者有个疑问,对于一个很多字段的结构体,或者很长的数组,每个key都遍历一遍字符串,时间复杂度不是又回到O(n^2) 了么,怎么快得了?于是乎,这个库提供了几个针对数组和对象的api,通过回调函数的方式,把对象的绑定过程交给了用户,这样遍历一次json串就能完成值的绑定。

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

我们以json object的解析过程为例来讲解下:遍历json object的每一个key,针对每个key,调用传入的回调方法。它的第三个参数也可以指定路径,我们可以从json的任意层级开始执行callback函数。

代码语言:javascript复制
func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error) {
//首先定位到层级
if len(keys) > 0 {
if off := searchKeys(data, keys...); off == -1 {
if off := nextToken(data[offset:]); off == -1 {
return MalformedObjectError
} else if offset  = off; data[offset] != '{' {
return MalformedObjectError
} else {
offset  
}

for offset < len(data) {
switch data[offset] {
case '"':
offset   // accept as string and skip opening quote
case '}':
return nil // we found the end of the object; stop and return success
default:
return MalformedObjectError
}
//匹配到key的字符串结尾,说明正常的key

// Step 1: find the next key
// Step 2: skip the colon
key, keyEscaped = data[offset:offset off-1], esc
// Step 3: find the associated value, then invoke the callback
if value, valueType, off, err := Get(data[offset:]); err != nil {
if err := callback(key, value, valueType, offset off); err != nil { 
// Step 4: skip over the next comma to the following token, or stop if we hit the ending brace

如果遇到合法的key,按照下面四步来执行回调函数:

1,找到这key

2,跳过冒号

3,对key对应的value执行callback函数

4,跳过逗号,进行下一个key的遍历,直到遇到结构体的结尾。

以上就是这个库的核心逻辑,它本质上提供了一个遍历json的思路,把值绑定交给了用户来操作,快也只是在指定条件下快,难怪有些大佬吐槽它是为了跑分而生,但是对于只想获取某些路径特定参数的场景,它确实是一个不错的选择。

0 人点赞