golang json序列化/反序列化的轮子一大票,很多人都吐槽官方库耗性能,但是性能耗在哪里?既然那么多优秀的开源库存在,golang官方包为啥不更新?带着这个疑问,基于go1.19分析下它的源码。
首先看下Marshal()函数,它的源码位于:encoding/json/encode.go,在看源码之前,我们先看下注释,这是最详尽的文档。
1,它允许每一种类型自定义序列化和反序列化方法,支持两种类型json和text,优先级如下:
A,如果字段不是nil,并且定义MarshalJSON方法就调用它的方法
B,如果没有定义上述这个方法,定义了encoding.TextMarshaler 就会调用,没有nil限制
C,否则按照官方的序列化方案按照下列规则序列化。
2,Marshal()函数会使用以下的基于类型的默认编码格式:
A,布尔类型编码为json布尔类型;
B,浮点数、整数和json.Number类型编码为json数字类型;
C,字符串类型编码为json字符串;UTF-8编码
D,数组和切片类型编码为json数组,但[]byte编码为base64编码字符串,nil切片编码为null;
E,结构体类型编码为json对象,每一个可导出字段(首字母大写)会变成该对象的一个成员。
F,不合法的字节会被编码成unicode。字符会被HTMLEscape 进行转码,"<", ">", "&", U 2028, and U 2029 会被替换成 "u003c","u003e", "u0026", "u2028", and "u2029"。所以json在Html <script>标签内是安全的,如果不想被转码,使用SetEscapeHTML(false).
代码语言:javascript复制// replacing invalid bytes with the Unicode replacement rune.
// So that the JSON will be safe to embed inside HTML <script> tags,
// the string is encoded using HTMLEscape,
// which replaces "<", ">", "&", U 2028, and U 2029 are escaped
// to "u003c","u003e", "u0026", "u2028", and "u2029".
// This replacement can be disabled when using an Encoder,
// by calling SetEscapeHTML(false).
G,json的key 默认是字段名,但是如果结构体设置了tag,key受到tag的影响,tag可以是逗号分割的多个字段。比如"omitempty",跳过空值。tag是"-"的字段会被跳过。如果json的key本身是 "-"可以在后面加一个逗号。
代码语言:javascript复制// Field int `json:"-,"`
H,tag的string选项会把值序列化成json的字符串
代码语言:javascript复制 StringInt int64 `json:"StringInt,string"`
StringString string `json:"StringString,string"`
序列化的值是
代码语言:javascript复制"StringInt":"0","StringString":"""",
但是它适用的类型仅仅有 string, floating point,integer, or boolean types.
I,key的类型仅仅是Unicode letters, digits, and ASCII punctuation ,并且key不能是单双引号,反斜线,逗号。并且会对key做一些类型转换。
代码语言:javascript复制// subject to the UTF-8 coercion described for string values above:
// - keys of any string type are used directly
// - encoding.TextMarshalers are marshaled
// - integer keys are converted to strings
J,Channel, complex, and function values 不会被序列化,会返回UnsupportedTypeError 错误
K,带环的数据结构,序列化会返回错误
L,当嵌套字段的字段和同级字段名字冲突的时候,如果加tag后名字不一样,都序列化,否则选择外面的,忽略嵌套的,不报错
下面,我们详细看下源码:
代码语言:javascript复制func Marshal(v any) ([]byte, error) {
e := newEncodeState()
err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return nil, err
}
buf := append([]byte(nil), e.Bytes()...)
encodeStatePool.Put(e)
return buf, nil
}
首先初始化了编码状态机,然后进行序列化,默认是进行html转义的,将序列化结果append到输出buf上,最后用sync.pool缓存编码状态机,方便下次复用,减少小对象的申请释放提升序列化速度。初始化序列化状态机的代码如下,它先从sync.pool里取,如果取不到,新定义一个
代码语言:javascript复制var encodeStatePool sync.Pool
代码语言:javascript复制func newEncodeState() *encodeState {
if v := encodeStatePool.Get(); v != nil {
e := v.(*encodeState)
e.Reset()
if len(e.ptrSeen) > 0 {
panic("ptrEncoder.encode should have emptied ptrSeen via defers")
}
e.ptrLevel = 0
return e
}
return &encodeState{ptrSeen: make(map[any]struct{})}
}
定义的过程就是初始化了一个map,用来存在序列化的过程中遇到的指针,防止出现环,从而导致栈溢出,详细可以看序列化状态机的注释
代码语言:javascript复制type encodeState struct {
bytes.Buffer // accumulated output
scratch [64]byte
// Keep track of what pointers we've seen in the current recursive call
// path, to avoid cycles that could lead to a stack overflow. Only do
// the relatively expensive map operations if ptrLevel is larger than
// startDetectingCyclesAfter, so that we skip the work if we're within a
// reasonable amount of nested pointers deep.
ptrLevel uint
ptrSeen map[any]struct{}
}
它内嵌了匿名field:bytes.Buffer ,每次从pool里取出来后会调用buf的Reset方法,重置buf。
下面就是核心的序列化方法,如果遇到panic,并且error类型是jsonError,就会被捕获,否则,继续panic。
代码语言:javascript复制func (e *encodeState) marshal(v any, opts encOpts) (err error) {
defer func() {
if r := recover(); r != nil {
if je, ok := r.(jsonError); ok {
err = je.error
} else {
panic(r)
}
}
}()
e.reflectValue(reflect.ValueOf(v), opts)
return nil
}
然后通过反射获取类型信息,根据输入的选项来进行序列化。
代码语言:javascript复制func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) {
valueEncoder(v)(e, v, opts)
}
函数内部会根据类型,选用不同的序列化方法,其中序列化方法有三个参数,序列化状态机,反射的Value和选项
代码语言:javascript复制type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts)
代码语言:javascript复制func valueEncoder(v reflect.Value) encoderFunc {
return typeEncoder(v.Type())
在根据类型返回不同的序列化方法的过程中,也会充分利用sync.Map来缓存遇到过的类型和对应的序列化方法,避免重复的反射操作,来提升性能。
代码语言:javascript复制var encoderCache sync.Map // map[reflect.Type]encoderFunc
获取序列化方法的时候,优先从缓存获取,如果取不到,先找到序列化方法,然后缓存下来:
代码语言:javascript复制func typeEncoder(t reflect.Type) encoderFunc {
if fi, ok := encoderCache.Load(t); ok {
return fi.(encoderFunc)
}
fi, loaded := encoderCache.LoadOrStore(t, encoderFunc(func(e *encodeState, v reflect.Value, opts encOpts) {
wg.Wait()
f(e, v, opts)
}))
f = newTypeEncoder(t, true)
wg.Done()
encoderCache.Store(t, f)
由于json的类型是可以递归的,所以寻找序列化的过程也是递归进行的,外层缓存序列化方法到sync.Map的过程,通过waitGroup等待内层计算完毕后才缓存。内层计算本层的处理函数,计算完毕后告知外层,等待递归的请求。然后我们就进入到序列化的核心函数:
代码语言:javascript复制func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {
return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
if t.Implements(marshalerType) {
return marshalerEncoder
}
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) {
return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false))
}
if t.Implements(textMarshalerType) {
return textMarshalerEncoder
}
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t)
case reflect.Array:
return newArrayEncoder(t)
case reflect.Pointer:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
如果不是指针类型的话,检查它是否实现了类型接收器的序列化方法,其中marshalerType
代码语言:javascript复制marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem()
然后检查是否实现了指针接受器的序列化方法,接着检查非指针类型接收器和指针类型接收器的文本序列化方法
代码语言:javascript复制textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
如果类型实现了上述序列化方法,就会按照类型自己定义的序列化方法来序列化,否则就用系统默认的序列化方法来序列化。针对go的每一种内置类型,都定义了对应的序列化方法。比如uint
代码语言:javascript复制func uintEncoder(e *encodeState, v reflect.Value, opts encOpts) {
b := strconv.AppendUint(e.scratch[:0], v.Uint(), 10)
if opts.quoted {
e.WriteByte('"')
}
e.Write(b)
if opts.quoted {
e.WriteByte('"')
}
}
float32和float64的序列化方法是一张的,只是初始化值不一样
代码语言:javascript复制type floatEncoder int // number of bits
func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
代码语言:javascript复制var (
float32Encoder = (floatEncoder(32)).encode
float64Encoder = (floatEncoder(64)).encode
)
然后就是按照编码规则,将浮点数编码成字符串:
代码语言:javascript复制func (bits floatEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
f := v.Float()
if math.IsInf(f, 0) || math.IsNaN(f) {
e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, int(bits))})
}
if abs != 0 {
if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
fmt = 'e'
}
}
b = strconv.AppendFloat(b, f, fmt, -1, int(bits))
对于结构体类型来说,它是复合类型,会被序列化成json的map,它的类型和对应的序列化方法会被缓存到sync.Map,当然用的时候也优先尝试从map里获取。
代码语言:javascript复制func newStructEncoder(t reflect.Type) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t)}
return se.encode
}
代码语言:javascript复制func cachedTypeFields(t reflect.Type) structFields {
if f, ok := fieldCache.Load(t); ok {
return f.(structFields)
}
f, _ := fieldCache.LoadOrStore(t, typeFields(t))
return f.(structFields)
}
它的序列化方法本身就是按照json协议拼字符串,对于每一个field,会递归调用序列化方法来序列化。
代码语言:javascript复制func (se structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
next := byte('{')
FieldLoop:
for i := range se.fields.list {
f := &se.fields.list[i]
if opts.escapeHTML {
e.WriteString(f.nameEscHTML)
} else {
e.WriteString(f.nameNonEsc)
}
opts.quoted = f.quoted
f.encoder(e, fv, opts)
调用自定义序列化方法的过程如下,空指针,或者没实现序列化方法,会序列化成“null”,否则调用MarshalJSON方法
代码语言:javascript复制func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
if v.Kind() == reflect.Pointer && v.IsNil() {
e.WriteString("null")
return
}
m, ok := v.Interface().(Marshaler)
if !ok {
e.WriteString("null")
return
}
b, err := m.MarshalJSON()
if err == nil {
// copy JSON into buffer, checking validity.
err = compact(&e.Buffer, b, opts.escapeHTML)
}
最后会进行转义
代码语言:javascript复制func compact(dst *bytes.Buffer, src []byte, escape bool) error {
for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') {
if start < i {
dst.Write(src[start:i])
}
dst.WriteString(`u00`)
dst.WriteByte(hex[c>>4])
dst.WriteByte(hex[c&0xF])
start = i 1
}
自定义序列化需要实现接口:
代码语言:javascript复制type Marshaler interface {
MarshalJSON() ([]byte, error)
}
至此,官方json序列化方法,介绍完毕,我们可以看到,虽然尽量使用了缓存的方法,来提升复用和减少反射操作,但是,对象和序列化方法的对应关系,还是在运行时通过反射的方式建立来写入sync.Map的,这里有两个比较低效的操作,map的写入和反射建立类型与序列化方法对应关系。在明确知道类型的情况下,这个过程其实可以在编译时完成,减少运行时的消耗。在同一类型反复序列化的场景,官方的库通过缓存的方式,能够提升后面几次序列化的性能。