百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

分析Go中的各种高性能JSON解析库

liuian 2025-01-08 15:19 22 浏览

比较fastjsongjsonjsonparser的性能、优点和缺点。

本文深入分析了Go中的标准库如何解析JSON,然后探索了流行的JSON解析库、其特征,以及它们如何更好地帮助我们在不同场景中的开发。

我不打算调查JSON库的性能问题。然而,最近,我在我的项目上做了一个pprof,从下面的火焰图中发现,业务逻辑处理中一半以上的性能消耗是在JSON解析期间。因此,这篇文章出现了。

本文深入分析了Go中的标准库如何解析JSON,然后探索了流行的JSON解析库、其特征,以及它们如何更好地帮助我们在不同场景中的开发。

主要介绍以下库的分析(2024-06-13):

JSON Unmarshal

func Unmarshal(数据 []字节,v interface{})

“官方的JSON解析库需要两个参数:要序列化的对象和该对象的类型。在实际执行JSON解析之前,调用reflect.ValueOf来获取参数v的反射对象。然后,根据传入数据对象开头的非空字符来确定解析方法。”

func (d *decodeState) value(v reflect.Value) error {
     开关d.opcode {
     默认:
         恐慌(phasePanicMsg)
     // 数组
     案例扫描BeginArray:
...
     // 结构或地图
     案例扫描BeginObject:
...
   // 文字,包括int、string、float等。
     案例扫描BeginLiteral:
...
}
     返回零
}

如果解析对象以[开头,则表示这是一个数组对象,并将进入scanBeginArray分支;如果以{开头,则表示解析对象是结构或映射,然后进入scanBeginObject分支,以此为由。

子摘要

查看Unmarshal的源代码,可以看出大量反射被用来获取字段值。如果JSON嵌套,则需要递归反射来获取值。因此,可以想象表现很差。

然而,如果性能不受到高度重视,直接使用它是一个不错的选择。它具有完整的功能,官方团队不断对其进行了其中和优化。也许它的性能在未来版本中也会带来质的飞跃。它应该是唯一一个可以直接将JSON对象转换为Go结构的。

fastjson

这个库的特点是速度快,就像它的名字一样。它的介绍页是这样说的:

快。像往常一样,比标准编码/json快15倍。

它的用法也很简单,如下:

func main() {
     var p fastjson.Parser
v, _ := p.Parse(`{
                 "str": "bar",
                 "int":123,
                 浮动":1.23,
                 "bool":真的,
                 "arr": [1, "foo", {}]
}`)
fmt.Printf("foo=%s\n", v.GetStringBytes("str"))
fmt.Printf("int=%d\n", v.GetInt("int"))
fmt.Printf("float=%f\n", v.GetFloat64("float"))
fmt.Printf("bool=%v\n", v.GetBool("bool"))
fmt.Printf("arr.1=%s\n", v.GetStringBytes("arr", "1"))
}
 // 输出:
 // foo=bar
 // int=123
 // 浮动=1.230000
 // bool=true
 // arr.1=foo

要使用fastjson,首先将JSON字符串交给Parser解析器进行解析,然后通过Parse方法返回的对象检索它。如果它是一个嵌套对象,在将参数传递给Get方法时,您可以直接传递相应的父子键。

分析

fastjson的设计与标准库Unmarshal不同,它将JSON解析分为两部分:解析和获取。

Parse负责将JSON字符串解析为结构并返回它。然后从返回的结构中检索数据。解析过程是无锁的,因此如果您想同时调用解析,则需要使用ParserPool。

fastjson通过从上到下遍历JSON,将解析的数据存储在Value结构中来处理JSON:

type Value struct { o Object a []*Value s string t Type }

这个结构很简单:

  • o Object:表示解析的结构是一个对象。
  • a []*Value:表示解析的结构是一个数组。
  • s string:如果解析的结构既不是对象也不是数组,则其他类型的值将作为字符串存储在此字段中。
  • t Type:表示此结构的类型,可以是TypeObject、TypeArray、TypeString、TypeNumber等。
type Object struct { kvs []kv keysUnescaped bool } type kv struct { k string v *Value }

这种结构存储对象的递归结构。在上述示例中解析JSON字符串后,结果结构如下所示:

代码

在实现方面,缺乏反射代码使整个解析过程非常干净。让我们直接看看解析的中心部分:

func parseValue(s string, c *cache, depth int) (*Value, string, error) {
     如果 len(s) == 0 {
         返回nil,s,fmt.Errorf(“无法解析空字符串”)
}
深度++
     // json字符串的最大深度不能超过MaxDepth
     如果深度 > MaxDepth {
         返回nil,s,fmt.Errorf(“对于嵌套的JSON来说深度太大;它超过%d”,MaxDepth)
}
     // 解析对象
     如果 s[0] == '{' {
v, tail, err := parseObject(s[1:], c, depth)
         如果是,那就是吧!= nil {
             返回nil,tail,fmt.Errorf(“无法解析对象:%s”,err)
}
         返回v,尾巴,零
}
     // 解析数组
     if s[0] == '[' {
...
}
     // 解析字符串
     如果 s[0] == '"' {
...
}
...
     返回v,尾巴,零
}

parseValue将根据字符串的第一个非空字符确定要解析的类型。在这里,一个对象类型用于解析:

func parseObject(s string, c *cache, depth int) (*Value, string, error) {
...
o := c.getValue()
o.t = 类型对象
o.o.reset()
     为了{
         var err 错误
         // 获取对象结构中的kv对象
kv := o.o.getKV()
...
         // 解析键值

kv.k, s, err = parseRawKey(s[1:])
...
         // 递归解析值
kv.v, s, err = parseValue(s, c, depth)
...
         // 如果遇到,请继续解析
         如果 s[0] == ',' {
s = s[1:]
             继续
}
         // 解析完成
         如果 s[0] == '}' {
             返回o,s[1:],nil
}
         返回nil, s, fmt.Errorf(“在对象值后缺少',”)
}
}

parseObject函数也很简单。它将在循环中获取键值,然后递归调用parseValue函数,从上到下解析该值,逐个解析JSON对象,直到最后遇到}

子摘要

通过上述分析,可以看出fastjson的实现要简单得多,并且比标准库具有更高的性能。使用解析解析JSON树后,可以多次重复使用,避免重复解析和提高性能。

然而,其功能非常简陋,缺乏常见的操作,如JSON到结构或JSON到地图转换。如果您只想简单地从JSON中检索值,那么使用此库非常方便。但是,如果您想将JSON值转换为结构,则需要自己手动设置每个值。

GJSON

在我的测试中,尽管GJSON的性能不像fastjson那样极端,但其功能非常完整,性能也相当不错。接下来,让我简要介绍一下GJSON的功能。

GJSON的用法类似于fastjson;它也非常简单。只需传递JSON字符串和需要作为参数获得的值。

json := `{"name":{"first":"li","last":"dj"},"age":18}`
姓氏:= gjson.Get(json,“name.last”)

除了此功能外,还可以执行简单的模糊匹配。它支持通配符*?在键中。*匹配任意数量的字符,而?匹配单个字符,如下所示:

json := `{
     "name":{"first":"Tom", "last": "Anderson"},
     年龄:37岁,
     “孩子”:[“萨拉”,“亚历克斯”,“杰克”]
}`
fmt.Println("third child*:", gjson.Get(json, "child*.2"))
fmt.Println(“第一个c?ild:", gjson.Get(json, "c?ildren.0"))
  • child*.2:首先,child*匹配children.2读取第三个元素;
  • c?ildren.0c?ildren匹配children.0读取第一个元素;

除了模糊匹配外,它还支持修饰符操作。

json := `{
     "name":{"first":"Tom", "last": "Anderson"},
     年龄:37岁,
     “孩子”:[“萨拉”,“亚历克斯”,“杰克”]
}`
fmt.Println("third child*:", gjson.Get(json, "children|@reverse"))

children|@reverse 首先阅读数组“children”,然后使用修饰符“@reverse”来反转它并返回输出。

nestedJSON := `{"nested": ["one", "two", ["three", "four"]]}` fmt.Println(gjson.Get(nestedJSON, "nested|@flatten"))

@flatten扁平化nested到外部数组的数组的内部数组,并返回:

["一," "二," "三," "四"]

还有其他一些令人兴奋的功能,您可以在官方文档中查看。

分析

gjson的Get方法参数包括一个JSON字符串和一个路径,表示要获取的JSON值的匹配路径。

gjson中,解析分为两部分,因为它需要满足解析场景的许多定义。在遍历JSON字符串之前,您需要解析路径。

如果您在解析过程中遇到可以匹配的值,它将直接返回,无需继续跟踪。如果匹配多个值,则将始终遍历整个JSON字符串。如果您遇到无法在JSON字符串中匹配的路径,则必须遍历完整的JSON字符串。

在解析过程中,解析内容不会保存在像fastjson这样的结构中,这种结构可以重复使用。因此,当您调用GetMany返回多个值时,您需要多次遍历JSON字符串,以便效率相对较低。

重要的是要知道@flatten函数不会验证JSON。这意味着,即使输入字符串不是有效的JSON,它仍然会被解析。因此,用户需要仔细检查输入是否为有效的JSON,以避免潜在问题。

代码

func Get(json,路径字符串)结果{
// 解析路径 
     if len(path) > 1 {
...
}
var i int
var c = &parseContext{json: json}
     if len(path) >= 2 && path[0] == '.' && path[1] == '.'{
c.lines = 真实
parseArray(c,0,路径[2:])
} 否则 {
// 根据不同的对象进行解析,并在这里循环,直到找到'{'或'['
         for ; i < len(c.json); i++ {
             如果 c.json[i] == '{' {
i++

解析对象(c,i,路径)
                 打破
}
             如果 c.json[i] == '[' {
i++
解析数组(c,i,路径)
                 打破
}
}
}
     如果c.piped {
res := c.value.Get(c.pipe)
res.Index = 0
         返回
}
fillIndex(json,c)
     返回c.value
}

在Get方法中,您可以看到一个用于解析各种路径的长代码字符串。然后,for循环连续遍历JSON,直到在执行相应的逻辑处理之前找到“{”或“[”。

func parseObject(s string, c *cache, depth int) (*Value, string, error) {
...
o := c.getValue()
o.t = 类型对象
o.o.reset()
     为了{
         var err 错误
         // 获取对象结构中的kv对象
kv := o.o.getKV()
...
         // 解析键值

kv.k, s, err = parseRawKey(s[1:])
...
         // 递归解析值
kv.v, s, err = parseValue(s, c, depth)
...
         // 如果遇到,请继续解析
         如果 s[0] == ',' {
s = s[1:]
             继续
}
         // 解析完成
         如果 s[0] == '}' {
             返回o,s[1:],nil
}
         返回nil, s, fmt.Errorf(“在对象值后缺少',”)
}
}

在审查parseObject代码时,目的不是教授JSON解析或字符串遍历,而是说明一个糟糕的情况。嵌套循环和连续if语句可能会让人不知所措,可能会提醒您在工作中遇到的同事的代码。

子摘要

优点:

  1. 性能:与标准库相比,jsonparser的性能相对较好。
  2. 灵活性:它提供各种检索方法和可定制的返回值,使其非常方便。

缺点:

  1. 没有JSON验证:它不检查JSON输入的正确性。
  2. 代码气味:代码结构繁琐且难以阅读,使维护具有挑战性。

笔记

当解析JSON以检索值时,GetMany函数将根据指定的键多次遍历JSON字符串。将JSON转换为地图可以减少遍历次数。

结论

虽然jsonparser具有显著的性能和灵活性,但它缺乏JSON验证和复杂、难以阅读的代码结构存在重大缺点。如果您需要经常解析JSON和检索值,请考虑性能和代码可维护性之间的权衡。

json解析器

分析

jsonparser还处理输入JSON字节切片,并允许通过传递多个键快速定位和返回值。

与GJSON类似,jsonparser不会像fastjson那样在数据结构中缓存解析的JSON字符串。然而,当需要解析多个值时,EachKey函数可以通过JSON字符串在一次路径中解析多个值。

如果找到匹配的值,jsonparser将立即返回,无需进一步遍历。对于许多匹配,它遍历整个JSON字符串。如果路径与JSON字符串中的任何值不匹配,它仍然会遍历整个字符串。

jsonparser在JSON遍历期间使用循环来减少递归的使用,减少调用堆栈深度,并提高性能。

在功能方面,ArrayEachObjectEachEachKey函数允许传递自定义函数以满足特定需求,大大增强了jsonparser的实用性。

jsonparser的代码简单明了,易于分析。有兴趣的人可以自己检查。

子摘要

与标准库相比,jsonparser的高性能可以归因于:

  1. 使用循环来最小化递归。
  2. 避免使用反射,与标准库不同。
  3. 找到相应的键值后立即退出,无需进一步递归。
  4. 在传递的JSON字符串上操作,而不分配新空间,从而减少内存分配。

此外,API设计很方便。ArrayEachObjectEachEachKey等函数允许传递自定义函数,解决实际业务开发中的许多问题。

然而,jsonparser有一个重大缺点:它不验证JSON。如果输入不是有效的JSON,jsonparser将不会检测到它。

性能比较

解析小JSON字符串

解析大约190字节的简单JSON字符串

190字节JSON测试结果

解析中号JSON字符串

解析一个中等复杂度的JSON字符串,大约2.3KB

2.3KB JSON测试结果

解析大型JSON字符串

解析高复杂度的JSON字符串,大约2.2MB

2.2MB JSON测试结果

摘要

在这次比较中,我分析了几个高性能JSON解析库。很明显,这些图书馆有几个共同的特征:

  • 他们避免使用反射。
  • 他们通过按顺序遍历JSON字符串的字节来解析JSON。
  • 他们通过直接解析输入JSON字符串来最小化内存分配。
  • 他们为了性能而牺牲了一些兼容性。

Despite these trade-offs, each library offers unique features. The fastjsonAPI is the simplest to use; GJSON offers fuzzy searching capabilities and high customizability; jsonparser supports inserting callback functions during high-performance parsing, providing a degree of convenience.

对于我的用例,即简单地从具有预定字段和偶尔自定义操作的HTTP响应JSON字符串中解析特定字段,jsonparser是最合适的工具。

因此,如果性能与您有关,请考虑根据您的业务需求选择JSON解析器。

相关推荐

Optional是个好东西,如果用错了就太可惜了

原文出处:https://xie.infoq.cn/article/e3d1f0f4f095397c44812a5be我们都知道,在Java8新增了一个类-Optional,主要是用来解决程...

IDEA建议:不要在字段上使用@Autowire了!

在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Fieldinjectionisnotrecommended.纳尼?我天天用,咋...

Spring源码|Spring实例Bean的方法

Spring实例Bean的方法,在AbstractAutowireCapableBeanFactory中的protectedBeanWrappercreateBeanInstance(String...

Spring技巧:深入研究Java 14和SpringBoot

在本期文章中,我们将介绍Java14中的新特性及其在构建基于SpringBoot的应用程序中的应用。开始,我们需要使用Java的最新版本,也是最棒的版本,Java14,它现在还没有发布。预计将于2...

Java开发200+个学习知识路线-史上最全(框架篇)

1.Spring框架深入SpringIOC容器:BeanFactory与ApplicationContextBean生命周期:实例化、属性填充、初始化、销毁依赖注入方式:构造器注入、Setter注...

年末将至,Java 开发者必须了解的 15 个Java 顶级开源项目

专注于Java领域优质技术,欢迎关注作者:SnailClimbStar的数量统计于2019-12-29。1.JavaGuideGuide哥大三开始维护的,目前算是纯Java类型项目中Sta...

字节跨平台框架 Lynx 开源:一个 Web 开发者的原生体验

最近各大厂都在开源自己的跨平台框架,前脚腾讯刚宣布计划四月开源基于Kotlin的跨平台框架「Kuikly」,后脚字节跳动旧开源了他们的跨平台框架「Lynx」,如果说Kuikly是一个面向...

我要狠狠的反驳“公司禁止使用Lombok”的观点

经常在其它各个地方在说公司禁止使用Lombok,我一直不明白为什么不让用,今天看到一篇文章列举了一下“缺点”,这里我只想狠狠地反驳,看到列举的理由我竟无言以对。原文如下:下面,结合我自己使用Lomb...

SpringBoot Lombok使用详解:从入门到精通(注解最全)

一、Lombok概述与基础使用1.1Lombok是什么Lombok是一个Java库,它通过注解的方式自动生成Java代码(如getter、setter、toString等),从而减少样板代码的编写,...

Java 8之后的那些新特性(六):记录类 Record Class

Java是一门面向对象的语言,而对于面向对象的语言中,一个众所周知的概念就是,对象是包含属性与行为的。比如HR系统中都会有雇员的概念,那雇员会有姓名,ID身份,性别等,这些我们称之为属性;而雇员同时肯...

为什么大厂要求安卓开发者掌握Kotlin和Jetpack?优雅草卓伊凡

为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡一、Kotlin:Android开发的现代语言选择1.1Kotlin是什么?Kotlin是由...

Kotlin这5招太绝了!码农秒变优雅艺术家!

Kotlin因其简洁性、空安全性和与Java的无缝互操作性而备受喜爱。虽然许多开发者熟悉协程、扩展函数和数据类等特性,但还有一些鲜为人知的特性可以让你的代码从仅仅能用变得真正优雅且异常简洁。让我们来看...

自行部署一款免费高颜值的IT资产管理系统-咖啡壶chemex

在运维时,ICT资产太多怎么办,还是用excel表格来管理?效率太低,也不好多人使用。在几个IT资产管理系统中选择比较中,最终在Snipe-IT和chemex间选择了chemex咖啡壶。Snip...

PHP对接百度语音识别技术(php对接百度语音识别技术实验报告)

引言在目前的各种应用场景中,语音识别技术已经越来越常用,并且其应用场景正在不断扩大。百度提供的语音识别服务允许用户通过简单的接口调用,将语音内容转换为文本。本文将通过PHP语言集成百度的语音识别服务,...

知识付费系统功能全解析(知识付费项目怎么样)

开发知识付费系统需包含核心功能模块,确保内容变现、用户体验及运营管理需求。以下是完整功能架构:一、用户端功能注册登录:手机号/邮箱注册,第三方登录(微信、QQ)内容浏览:分类展示课程、文章、音频等付费...