JSON解析-如何处理的又快又好?
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
在考虑MQ消费模式的优化方案期间,发现有很多关于JSON解析的相关操作,针对存在json解析的场景写了篇小小滴看法~
概述:原生json库的分析、使用事项及业务场景优化方向
包含对JSON 库 Unmarshal、sync 库 pool 的剖析
版本: go 1.18.3
How to do it well?
Golang官方自带一个 encoding/json 库,可以用于JSON的加密与解析,其中Marshal、Unmarshal也是最常使用的两个方法,那么如何让解析做得更好?
Unmarshal的解析步骤
下图用于阐述Unmarshal的解析过程:
暂时无法在飞书文档外展示此内容
精度丢失的坑
这是 json 与 go 对应的数据类型
Go | JSON |
---|---|
bool | 对应JSON布尔类型 |
float64 | 对应JSON数字类型 |
string | 对应JSON字符串类型 |
[]interface{} | 对应JSON数组 |
map[string]interface{} | 对应JSON对象 |
nil | 对应JSON的null |
json的规范中,对于数字类型是不区分整形和浮点型的,所以go采用float64来映射,但有个隐患,如果超出float64的安全整数范围,那么就会精度丢失。
当使用 interface{} 接收整数,再次 Marshal 需要注意精度丢失的问题。
- float64最大的安全整数是52位尾数全为1且指数部分为最小 0x001F FFFF FFFF FFFF
- float64可以存储的最大整数是52位尾数全位1且指数部分为最大 0x07FEF FFFF FFFF FFFF
- 十进制有效数字在16位(max = 9007199254740991),超过就很可能精度丢失
解决办法:
- 明确使用int接收,不使用interface{}
- 使用decoder的方式,设置useNumber=true,解析大数
decoder := json.NewDecoder(bytes.NewReader(request))
decoder.UseNumber() // set useNumber = true
err := decoder.Decode(&req)
设置了UseNumber后,会将数字当作json.Number类型处理,底层是string。
// A Number represents a JSON number literal.
type Number string
// String returns the literal text of the number.
func (n Number) String() string { return string(n) }
// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
// json.newTypeEcnode -- 记录对应类型的序列化方法
使用tag来实现额外的需求(在指定名称后添加 , 与 参数):
- omitempty:
json:"name,omitempty"
可以在数值为0或空的时候忽略
- string/number/boolean:
json:"name,string"
可以制定序列化的时候类型转为string,除此以外还可以选择 number、boolean
小结
综上可得,我们使用接受者要注意以下几点:
- 接受者需要是指针类型
- 如果明确了类型,使用struct & 设置json标签为“-”,有助于过滤无需字段,同时相比map可以确定字段类型,减少判断类型的步骤,效果更好
- 默认使用的
decodeState
的useNumber
为false,且接受者用的类型是interface{},有精度丢失的情况(当超出float64的安全整数范围时)
- decoder 与 Unmarshal选择:
json.Decoder
适合io.Reader
流,或者需要从数据流中解码多个值
json.Unmarshal
适合内存中已有 JSON 数据
- 通过编写
MarshalJSON()([]byte, error)
和UnmarshalJSON(b []byte) error
来实现自定义JSON序列化和反序列化
- 不要频繁的序列化和反序列化,尽量一次解决
- 大量的递归和反射影响性能,可以优化(类似protobuf生成代码解析)
How to get faster?
大多情况下,反序列化对象的使用过程可以类比做下面的漫画(拿出小学毕生功力所绘~🐶):
(一群小朋友吃完就把饭盒扔了,这样倒垃圾的次数就很多了)
(如果能把饭盒重复利用下,那就完美了,记得把餐盘洗干净)
短时间反序列化大量对象会对GC带来很大压力,我们可以使用sync.pool
来优化这一现象,实现临时对象的复用,像gin
框架就是复用了context
。
sync.pool介绍
type Pool struct {
noCopy noCopy // A Pool must not be copied after first use.
// 实际指向 []poolLocal,数组大小等于 Process 的数量 local unsafe.Pointer
localSize uintptr
// GC 时,victim 和 victimSize 会分别接管 local 和 localSize
// 延缓一次GC
victim unsafe.Pointer
victimSize uintptr
New func() interface{} // 需要实现的创建对象的方法
}
sync.Pool 是以缓存池的形式,实现对象复用,减少 GC 次数
(大致内部结构如下图)
- poolDequeue 是一个环形队列 ,使用位运算区分 head 和 tail 的值,采取 atomic 和 CAS 对值进行操作,使用了原子操作来替代锁机制。
- 生产者可以从 head 写/插入、从 head 读/删除。
- 消费者仅可从 tail 读/删除。
详细学习参考官方文档,这里只是简单说了其用法。
看一下效果:
这是一个小测试就能看出添加了sync.pool在面对大量反序列化时候的优势:
package main
import (
"encoding/json"
"sync"
"testing"
)
type Say struct {
ID int `json:"id"`
Name string `json:"name"`
Dmap map[string]string `json:"dmap"`
}
var pool = sync.Pool{
New: func() interface{} {
return &Say{}
},
}
var j, _ = json.Marshal(
Say{
Name: "dag",
ID: 25,
Dmap: map[string]string{
"cool": "hot",
},
})
func BenchmarkUnmarshalWithPool(b *testing.B) {
g := sync.WaitGroup{}
g.Add(b.N)
for i := 0; i < b.N; i++ {
go func() {
s := pool.Get().(*Say)
_ = json.Unmarshal(j, s)
pool.Put(s)
g.Done()
}()
}
g.Wait()
}
func BenchmarkUnmarshalWithStruct(b *testing.B) {
g := sync.WaitGroup{}
g.Add(b.N)
for i := 0; i < b.N; i++ {
go func() {
s := &Say{}
_ = json.Unmarshal(j, s)
g.Done()
}()
}
g.Wait()
}
执行 go test -bench . -benchmen
:
Say 结构体内存占用较小,内存分配耗时短,如果是个大对象,则差距会更加明显。
需要注意的是,由于sync.pool存储的是临时对象,所以不能控制被回收的时间,不建议存储连接类型的。如果想要存储,则可以选择借助SetFInalize进行收尾工作。
SetFinalizer 的定义:func SetFinalizer(x, f interface{})
- 参数 x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针。
- 参数 f 必须是一个函数,可以直接用 x 类型值赋值的参数,也可以有任意个被忽略的返回值,比如
func funcName(x *X)
。
- SetFinalizer 函数可以将 x 的终止器设置为 f,当垃圾收集器发现 x 不能再直接或间接访问时,它会清理 x 并调用 f(x)。
下面这个例子可以看出对象回收前的行为
package main
import (
"fmt"
"runtime"
"testing"
"time"
)
type Conn struct {
Name string `json:"name"`
}
func (c *Conn) Start() {
fmt.Println("start:", c.Name)
}
func (c *Conn) close() {
fmt.Println("close:", c.Name)
}
func closeConn(c *Conn) {
c.close()
}
func InitConn(name string) *Conn {
c := &Conn{Name: name}
runtime.SetFinalizer(c, closeConn)
return c
}
func TestConn(t *testing.T) {
c := InitConn("hello")
c.Start()
time.Sleep(time.Second)
runtime.GC() // 手动GC
}
结果:
=== RUN TestConn
start: hello
close: hello
--- PASS: TestConn (1.01s)
使用建议
- 取出使用的对象要先进行
reset
,Put
回去的对象要确定后续逻辑不再使用到
- 设计的
new
方法要保证并发安全
- 建议不要存放链接等对象,防止协程泄漏,如果要的话,请设置收尾工作
总结
- Unmarshal的接受体需要传
指针
,变量是可导出的
(即首字母大写)
- 不要频繁的序列化和反序列化,尽量一次解决
- 了解Json与Go的数据类型映射关系,避免踩坑大数的情况
- 根据情况选择 decoder 与 Unmarshal,如果明确了类型,使用struct & 设置json标签为“-”,有助于过滤无需字段,同时相比map可以确定字段类型,减少判断类型的步骤,效果更好
- 巧用
sync.pool
让对象实现复用,减少GC次数,需遵循使用建议
第三方JSON库拓展
从一些第三方库能学习到对原有JSON库的优化方向,
-
通过声明IDL,预先生成序列化、反序列化代码,但是对业务侵入性强:
- github.com/pquerna/ffjson
- github.com/mailru/easy…
-
分配优化(避免复制、切片重用)、流式拉取(避免一次读入大数组)、特殊处理字符串
转载自:https://juejin.cn/post/7139082554304364581