阿里妹导读
引言
你真的了解 Go 标准库吗?
var s string
err := json.Unmarshal([]byte(`"Hello, world!"`), &s)
assert.NoError(t, err)
fmt.Println(s)
// 输出:
// Hello, world!
解:其实标准库解析不仅支持是对象、数组,同时也可以是字符串、数值、布尔值以及空值,但需要注意,上面字符串中的双引号不能缺,否则将不是一个合法的 json 序列,会返回错误。
cert := struct {
Username string `json:"username"`
Password string `json:"password"`
}{}
err = json.Unmarshal([]byte(`{"UserName":"root","passWord":"123456"}`), &cert)
if err != nil {
fmt.Println("err =", err)
} else {
fmt.Println("username =", cert.Username)
fmt.Println("password =", cert.Password)
}
// 输出:
// username = root
// password = 123456
解:如果遇到大小写问题,标准库会尽可能地进行大小写转换,即:一个 key 与结构体中的定义不同,但忽略大小写后是相同的,那么依然能够为字段赋值。
为什么使用第三方库,标准库有哪些不足?
API 不够灵活:如没有提供按需加载机制等;
三方库哪些家强?
如何结合业务去选型?
库名 | encoder | decoder | compatible | star 数 (2023.04.19) | 社区维护性 |
StdLib(encoding/json)[2] | ✔️ | ✔️ | N/A | - | - |
FastJson(valyala/fastjson)[3] | ✔️ | ✔️ | ❌ | 1.9k | 较差 |
GJson(tidwall/gjson)[4] | ✔️ | ✔️ | ❌ | 12.1k | 较好 |
JsonParser(buger/jsonparser)[5] | ✔️ | ✔️ | ❌ | 5k | 较差 |
JsonIter(json-iterator/go)[6] | ✔️ | ✔️ | 部分兼容 | 12.1k | 较差 |
GoJson(goccy/go-json)[7] | ✔️ | ✔️ | ✔️ | 2.2k | 较好 |
EasyJson(mailru/easyjson)[8] | ✔️ | ✔️ | ❌ | 4.1k | 较差 |
Sonic(bytedance/sonic)[9] | ✔️ | ✔️ | ✔️ | 4.1k | 较好 |
评判标准
评判标准包含三个维度:
性能:内部实现原理是什么,是否使用反射机制;
稳定性:考虑到要投入生产使用,必须是一个较为稳定的三方库;
泛型(generic)编解码:json 没有对应的 schema,只能依据自描述语义将读取到的 value 解释为对应语言的运行时对象,例如:json object 转化为 Go map[string]interface{};
定型(binding)编解码:json 有对应的 schema,可以同时结合模型定义(Go struct)与 json 语法,将读取到的 value 绑定到对应的模型字段上去,同时完成数据解析与校验;
查找(get)& 修改(set):指定某种规则的查找路径(一般是 key 与 index 的集合),获取需要的那部分 json value 并处理。
功能评测
性能评测
Large[13](635KB, 10000+ key, 6 layers)
func BenchmarkEncoder_Generic_StdLib(b *testing.B) {
_, _ = json.Marshal(_GenericValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(_GenericValue)
}
}
func BenchmarkEncoder_Binding_StdLib(b *testing.B) {
_, _ = json.Marshal(&_BindingValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(&_BindingValue)
}
}
func BenchmarkEncoder_Parallel_Generic_StdLib(b *testing.B) {
_, _ = json.Marshal(_GenericValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = json.Marshal(_GenericValue)
}
})
}
func BenchmarkEncoder_Parallel_Binding_StdLib(b *testing.B) {
_, _ = json.Marshal(&_BindingValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = json.Marshal(&_BindingValue)
}
})
}
具体的指标数据和统计结果,可参考benchmark_readme [14],总体结论如下:
常见优化思路有哪些?
定型编解码
// 函数缓存
type cache struct {
functions map[*rtype]function
lock sync.Mutex
}
var (
global = func() [caches]*cache {
var caches [caches]*cache
for idx := range caches {
caches[idx] = &cache{functions: make(map[*rtype]function, 4)}
}
return caches
}()
)
func load(typ *rtype) (function, bool) {
do, ok := global[uintptr(unsafe.Pointer(typ))%caches].functions[typ]
return do, ok
}
func save(typ *rtype, do function) {
cache := global[uintptr(unsafe.Pointer(typ))%caches]
cache.lock.Lock()
cache.functions[typ] = do
cache.lock.Unlock()
}
泛型编解码
如果用一种与 json AST 更贴近的数据结构来描述,不但可以让转换过程更加简单,甚至可以实现 lazy-load 。
复用编码缓冲区
type buffer struct {
data []byte
}
var bufPool = sync.Pool{
New: func() interface{} {
return &buffer{data: make([]byte, 0, 1024)}
},
}
// 复用缓冲区
buf := bufPool.Get().(*buffer)
data := encode(buf.data)
newBuf := make([]byte, len(data))
copy(newBuf, buf)
buf.data = data
bufPool.Put(buf)
Sonic 库为什么性能好?
原理调研
离线场景:针对 Go 语言编译优化的不足,Sonic 核心计算函数使用 C 语言编写,使用 Clang 的深度优化编译选项,并开发了一套 asm2asm 工具,将完全优化的 x86 汇编翻译成 plan9 汇编,加载到 Golang 运行时,以供调用。
针对泛型编解码,基于 map 开销较大的考虑,Sonic 实现了更符合 json 结构的树形 AST;通过自定义的一种通用的泛型数据容器 sonic-ast 替代 Go interface,从而提升性能。
用 node {type, length, pointer} 表示任意一个 json 数据节点,并结合树与数组结构描述节点之间的层级关系。针对部分解析,考虑到解析和跳过之间的巨大速度差距,将 lazy-load 机制到 AST 解析器中,以一种更加自适应和高效的方式来减少多键查询的开销。
type Node struct {
v int64
t types.ValueType
p unsafe.Pointer
}
无栈内存管理:自己维护变量栈(内存池),避免 Go 函数栈扩展。
自动生成跳转表,加速 generic decoding 的分支跳转。
使用寄存器传参:尽量避免 memory load & store,将使用频繁的变量放到固定的寄存器上,如:json buffer、结构体指针;
业务实践
import "github.com/brahma-adshonor/gohook"
func main() {
// 在main函数的入口hook当前使用的json库(如encoding/json)
gohook.Hook(json.Marshal, sonic.Marshal, nil)
gohook.Hook(json.Unmarshal, sonic.Unmarshal, nil)
}
func TestEncode(t *testing.T) {
data := map[string]string{"&&": "<>"}
// 标准库
var w1 = bytes.NewBuffer(nil)
enc1 := json.NewEncoder(w1)
err := enc1.Encode(data)
assert.NoError(t, err)
// Sonic 库
var w2 = bytes.NewBuffer(nil)
enc2 := encoder.NewStreamEncoder(w2)
err = enc2.Encode(data)
assert.NoError(t, err)
fmt.Printf("%v%v", w1.String(), w2.String())
}
// 运行结果:
{"\u0026\u0026":"\u003c\u003e"}
{"&&":"<>"}
若有需要可以通过下面方式开启:
import "github.com/bytedance/sonic/encoder"
v := map[string]string{"&&":"<>"}
ret, err := encoder.Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":" \u003e"}}`
enc := encoder.NewStreamEncoder(w)
enc.SetEscapeHTML(true)
err := enc.Encode(obj)
import (
"reflect"
"github.com/bytedance/sonic"
"github.com/bytedance/sonic/option"
)
func init() {
var v HugeStruct
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
err := sonic.Pretouch(reflect.TypeOf(v))
// with more CompileOption...
err := sonic.Pretouch(reflect.TypeOf(v),
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
// you can set compile recursive loops in Pretouch for better stability in JIT.
option.WithCompileRecursiveDepth(loop),
// For a large nested struct, try to set a smaller depth to reduce compiling time.
option.WithCompileMaxInlineDepth(depth),
)
}
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/encoder"
// Binding map only
m := map[string]interface{}{}
v, err := encoder.Encode(m, encoder.SortMapKeys)
// Or ast.Node.SortKeys() before marshal
var root := sonic.Get(JSON)
err := root.SortKeys()
总结
不太推荐使用 Jsoniter 库,原因在于: Go 1.8 之前,官方 Json 库的性能就收到多方诟病。不过随着 Go 版本的迭代,标准 json 库的性能也越来越高,Jsonter 的性能优势也越来越窄。如果希望有极致的性能,应该选择 Easyjson 等方案而不是 Jsoniter,而且 Jsoniter 近年已经不活跃了。
为字节节省数十万核的 json 库 sonic:https://zhuanlan.zhihu.com/p/586050976
[1]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry
[2]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry
[3]https://github.com/valyala/fastjson?spm=ata.21736010.0.0.6e462b76wytEry
[4]https://github.com/tidwall/gjson?spm=ata.21736010.0.0.6e462b76wytEry
[5]https://github.com/buger/jsonparser?spm=ata.21736010.0.0.6e462b76wytEry
[6]https://github.com/json-iterator/go?spm=ata.21736010.0.0.6e462b76wytEry
[7]https://github.com/goccy/go-json?spm=ata.21736010.0.0.6e462b76wytEry
[8]https://github.com/mailru/easyjson?spm=ata.21736010.0.0.6e462b76wytEry
[9]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry
[10]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/bench.sh?spm=ata.21736010.0.0.6e462b76wytEry&file=bench.sh
[11]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/small.go?spm=ata.21736010.0.0.6e462b76wytEry&file=small.go
[12]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/medium.go?spm=ata.21736010.0.0.6e462b76wytEry&file=medium.go
[13]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/large.json?spm=ata.21736010.0.0.6e462b76wytEry&file=large.json
[14]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/README.md?spm=ata.21736010.0.0.6e462b76wytEry&file=README.md
[15]https://github.com/simdjson/simdjson?spm=ata.21736010.0.0.6e462b76wytEry
[16]https://github.com/minio/simdjson-go?spm=ata.21736010.0.0.6e462b76wytEry
[17]https://github.com/brahma-adshonor/gohook?spm=ata.21736010.0.0.6e462b76wytEry
[19]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry#compatibility
[20]https://github.com/bytedance/sonic/issues/172?spm=ata.21736010.0.0.6e462b76wytEry
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
文章引用微信公众号"阿里开发者",如有侵权,请联系管理员删除!