基准测试

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 操作性能又提升到了一个新的高度。