golang源码分析:encoding/json(1)

2023-09-06 19:15:07 浏览数 (2)

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的写入和反射建立类型与序列化方法对应关系。在明确知道类型的情况下,这个过程其实可以在编译时完成,减少运行时的消耗。在同一类型反复序列化的场景,官方的库通过缓存的方式,能够提升后面几次序列化的性能。

0 人点赞