首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Golang Json Marshal 源码分析

JSON 是?种轻量级的数据交换格式,强??简单,是流?的最主要的数据交换之?。Marshal 这?术语是指是将语?的内存对象,解析为 JSON 格式的字符串。本?主要分析在 Golang 语?原?包内,是如何实现将结构体解析为 JSON 字符串,分析代码基于 go 1.14.2 。

前置知识

语言类型

Marshal ?标是,将 Golang 的对象转换成符合 json 标准的字符串。 对象由结构体实例化?成,Golang 语?使?type 和 struct 关键词来定义结构体。结构体是复合类型,由其域成员嵌套组成树状结构。成员也会可能有 map 、array 、 struct 等复合类型。

代码语言:javascript
复制
stu := Student{ Name: "张三", Age: 18, High: true, sex: "男"} 

可以被解析为:

代码语言:javascript
复制
'{"name":"张三", "age":18, "high":"true", "set":"男"}'

从序列化的?度考虑,我们将 Golang 内部数据类型分为两类,基本类型和复合类型:

复合类型,需要?系列复杂递归流程?基本类型,才能?成字节序列。?简单的类型,可以直接?成字节序列。每种类型都有对应?种编码函数,取 Bool 类型举例,其编码函数为 boolEncoder,如下,只是简单的写? buffer ? “true/false”。其余除 String 内各种转义处理会略麻烦,其余都相对简单,不做具体分析。

代码语言:javascript
复制
func boolEncoder(e *encodeState, v reflect.Value, opts encOpts) {
    if opts.quoted {
        e.WriteByte('"')
    }
    if v.Bool() {
        e.WriteString("true")
    } else {
        e.WriteString("false")
    }
    if opts.quoted {
        e.WriteByte('"')
    }
}

反射

Golang 结构体序列化 JSON 的过程,利?了语?内置的反射的特性来实现不同数据类型的通?逻辑,所以我们要先对其?到的反射有?定的认知。

编程语??,每个对象可以有两个不同维度的属性,即类型和值,如 Golang 的 object 有 Type 和 Value。 Type 表示类型,每个?定义定义的结构体,是?种不同的类型,如结构体的组成不同,就是不同类型,?Value 装载具体对象的值。可以理解为,Value 装载了具体的字节块,? Type 对象装载了该字节块的模式, 字节块没有边 界,要按 Type 来格式化为对象。

Value 和 Type 在程序执?中,也是具象化的具体结构体对象,存储了具体的值,其内部采?冗余字段的?式来对不同类型提供?持。除了 Value 和 Type,还有个更?维度的属性 Kind,Kind 应当被理解为 Type 更??层的抽象, Type 类?为?汽?、公交?、救护?等,Kind 则表示机动?。Golang 总共定义了 27 种不同的 Kind,其值为 uint 类型的枚举值 。

reflect 包提供了较为完善的机制来?持使?反射的特性,如 Type 和 Value 都提供了 Kind()?法?来获取其属于的 Kind 常量。reflect.Value 可以装载任意类型的值,反射函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回?个装载着其动态值的 reflect.Value。通过使? Value 提供的 v.Elem()来获取 interface()或者 pointer 的 Value 值。使?反射,默认使?者(Json 序列化包)清楚其原始类型,否则会直接 panic,如对? Array、Chan、Map、Ptr、or Slice 类型调? Elem()。

代码语言:javascript
复制
type Type interface {
    // Key returns a map type's key type.
    // It panics if the type's Kind is not Map.
    Key() Type
    // Elem returns a type's element type.
    // It panics if the type's Kind is not Array, Chan, Map, Ptr,
or Slice.
    Elem() Type
}


// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value {
   // ....
}

对?户?定义的不同结构体??,其 reflect.Type 不?样。reflect.Type 之间的相互?较,会循环递归保证内部所有域确保?致。?户?定义的结构体和内置类型同样凑效。

代码语言:javascript
复制
reflect/type.go:2731
if comparable {
        typ.equal = func(p, q unsafe.Pointer) bool {
            for _, ft := range typ.fields {
                pi := add(p, ft.offset(), "&x.field safe")
                qi := add(q, ft.offset(), "&x.field safe")
                if !ft.typ.equal(pi, qi) {
                    return false
                 }
            }
            return true
        }
}

上述反射知识,有助于我们深?理解源码,此处先抛出 2 个相关问题:

  1. 上?提到的每种类型都有对应?种编码函数 encoderFunc,究竟是对不同的Kind,还是对应不同的Type?
  2. 下?会讲到序列化的缓存中间结果,那么缓存是针对不同的 Kind 还是针对不同的 Type 来缓存?

解析流程

Json Marshal 的流程,有两条主线,?条是利?Golang 反射原理,使?递归的解析结构体内所有的字段,?成字节序列,有两处递归;另外?条主线是,尽可能的缓存可复?的中间状态结果,以提?性能,有三处缓存。

从 序列化的?? marshal 出发,我们看到,其中 valueEncoder 内部会调?预处理过程 typeEncoder,valueEncoder 将会调? newTypeEncoder,?成每种类型对应的 encoderFunc。然后对每种类型调?的相对应的 encoderFunc 执?具体序列。

代码语言:javascript
复制
func (e *encodeState) reflectValue(v reflect.Value, opts encOpts)
{
valueEncoder(v)(e, v, opts)
}

参考下?代码,可以看到 switch t.Kind() 返回语?数据类型的,是对每种 Kind 有对应?种编码函数 encoderFunc,那上?的问题?是否可以回答了?理由是:encoderFunc 可以统?在 Kind 层,是因为每种 Kind 的处理逻辑是相同的,况且,因为 struct 是?户可以?定义的,所以具体的 Type 是?穷的,也不可能将执?逻辑为每种 Type 定义?种。

其实不然,虽然每种 Kind 的处理逻辑是相同的,但是每种 Type 所对应的值是不?样的,如代码中 newStructEncoder(t) 返回的是具体的 structEncoder 的?法,和该 struct 对应的 Type 的具体值有关联。 所以,上?的问题?的回答应该是对每?种 Type,?成基于 Type 的编码?法 se.encode(…)。

代码语言:javascript
复制
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
    // ?先,对实现了 Marshaler 或者 TextMarshaler的,且可以寻址的,直接
返回?定义的 encoder
    // 然后,对各种类型进?encode,复合类型继续调?typeEncoder递归处理
    switch t.Kind() {
    case reflect.Bool:
        return boolEncoder
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
       return intEncoder
       
       # 等等各种类型
   case reflect.Struct:
       return newStructEncoder(t)
   case reflect.Slice:
       return newSliceEncoder(t)
   case reflect.Ptr:
       return newPtrEncoder(t)
   default:
       return unsupportedTypeEncoder
   }
   //... ...
}

注:此处笔者对源码?多处命名表示质疑,如 valueEncoder 的?的是输出每种 Kind 的 encoderFunc,? valueEncoder 命名会让读代码者产??定困惑。还有调?链路 中,typeEncoder 的命名,它只是? newTypeEncoder 多了?层缓存,typeEncoder 更合适的应该是 cachedTypeEncoder,newTypeEncoder 应该恢复为 typeEncoder。 这样也会与源码中的 cachedTypeFields 保持?致。

主线?:两处递归

从源码看到,Marshal 流程,包含两处递归:

  1. 递归遍历结构体树状结构,对内部结点?成其对应类型编码器encodeFunc,或者开发者?定义的编码器(TextMarshaler、Marshaler),递归的结束条件是最终递归?基本类型,?成基本类型编码器。
  2. 启动类型编码器调?,依赖类型编码器函数内部递归,从根节点依次调?整棵树的序列化函数。递归的结束条件是递归?基本类型编码器,?成字符编码。

递归是为了处理复杂类型, 如类型编码器调?过程中,复合类型 Ptr(指针) 、Slice、(切?)、Array、Map 处理过程类似。Ptr 编码器函数 通过 t.Elem() 递归调?typeEncoder;Array/Slice 编码器函数通过 t.Elem() 递归调? typeEncoder;Map 稍微复杂,不但通过 t.Elem() 递归调? typeEncoder,其额外的操作是的对其 Key 进?处理,通过判断其 Key 类型,操作如下:

  1. Key 为 string 类型,直接存储为JSON字符序列的Key值。
  2. Key 为 ?string类型,且实现了 TextMarshaler,按 TextMarshaler 的实现处理,最终将?string的key转换为string类型,直接存储为JSON字符序列的Key值。
  3. 其他类型,输出不?持错误。因为常规的 JSON Key 也是string类型。下?代码,是 struct 的类型编码器递归调?过程,它?先遍历所有的成员,并通过f.encoder 来调?每个成员的编码函数,如果成员是复合类型,则会?层层深?调?,是?个较为完整的?度优先遍历:
代码语言: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]
        fv := v
        for _, i := range f.index {
            ......
            fv = fv.Field(i)
        }
    
        e.WriteByte(next)
        next = ','
        ......
        opts.quoted = f.quoted
         // 递归调?
        f.encoder(e, fv, opts)
    }
    if next == '{' {
        e.WriteString("{}")
    } else {
        e.WriteByte('}')
    }
}

递归调?,最终会形成树状调?结构体,对如下 Student 的结构体对应的对象,其最 终的递归树状图如下:

代码语言:javascript
复制
type Student struct {
    Name string `json:"name"`
    Age int
    CurrentClass *Class
    Classes []Class
    Friends map[string]Student
}

主线二:三处缓存

序列化的过程中,使?了三处全局缓存,来提?性能。其中 ?个 sync.poll 缓存了临 时对象,两个 sync.map 分别缓存了解析的中间过程编码函数,和结构体的分析及预 处理值。 注意这?是程序进程全局缓存,可以在进程内通?于序列化任意对象,这是因为 ?个进程中,Type 是唯?的,其?于序列化的属性是稳定不变的,可以全局通?。

sync.poll 存储 encodeState

sync.poll 是 Golang 官?提供?来缓存分配的内存的对象,以降低分配和 GC 压?。 序 列化中,encodeState 的?要作?是存储字符编码,其内部包含了 bytes.Buffer,由于 在 json.Marshal 在 IO 密集的业务程序中,通常会被?量的调?,如果不断的释放?成 新的 bytes.Buffer,会降低性能。 官?包的源码可以看到, encodeState 结构体被放 进 sync.poll 内(var encodeStatePool ),来保存和复?临时对象,减少内存分配, 降低 GC 压力。

代码语言:javascript
复制
// An encodeState encodes JSON into a bytes.Buffer.
type encodeState struct {
    bytes.Buffer // accumulated output
    scratch [64]byte
    // ?来避免层级递归层级过深,遇到层级过深的 时候,会主动失败。
    ptrLevel uint
    ptrSeen map[interface{}]struct{}
}

sync.poll 内部的 bytes.Buffer 提供可扩容的字节缓冲区,其实质是对切?的封装,结 构中包含?个 64 字节的?切?,避免?内存分配,并可以依据使?情况?动扩充。? 且,其空切? buf[:0] 在该场合下?常有?,是直接在原内存上进?操作,?常?效, 每次开始序列化之处,会将 Reset()。

代码语言:javascript
复制
func (b *Buffer) Reset() {
    b.buf = b.buf[:0]
    b.off = 0
    b.lastRead = opInvalid
}

解析初始,从 encodeStatePool 内获取”旧内存”,并进? Reset

代码语言:javascript
复制
func newEncodeState() *encodeState {
    if v := encodeStatePool.Get(); v != nil {
        e := v.(*encodeState)
        e.Reset()
        ...
        e.ptrLevel = 0
        return e
    }
    return &encodeState{ptrSeen: make(map[interface{}]struct{})}
}

复制代码

sync.map 存储 encoderFunc

代码语言:javascript
复制
var encoderCache sync.Map // map[reflect.Type]encoderFunc

在序列化过程中利?了反射的特性来处理不同类型的通?逻辑。在递归过程中,产?的不同的类型所对应的 encoderFunc, 都会被存储在全局的 sync.map 内。通过对中间过程产?的 encoderFunc 缓存,减少每次?成的开销。

如上?阐述中提到,Golang 通过反射,将对象两个维度的属性提取,即 reflect.Type、reflect.Value。在树状递归过程中,使?reflect.Value 来进?传递。使?reflect.Type 来作为 key 来缓存不同的 encoderFunc。此处回答下上?提出的问题?:存储只以 Type 为维度,举个例?,根据上? reflect.Type 描述可知道,含有不同数据成员的 struct 是对应不同的 Type,如果 structA 内部有嵌套其他 structB。structA、structB 会被独?的两个 key 被存储起来,查找 structA 时候,可以直接获取其对应的 encoderFunc。

在序列化 student 对象后,进? school 对象的序列化,由于包含 student 类型,全局 复?示例如下:

代码语言:javascript
复制
type School struct {
    Name string `json:"name"`
    Students []Student
}

sync.map 存储 struct 对应的 filed list

代码语言:javascript
复制
var fieldCache sync.Map // map[reflect.Type]structFields

程序执?过程中,结构体内部分成员属性是必然保持稳定,在序列化过程中可以被加 以利?来提?性能,如:

  1. 某个域,是否需要被序列化;
  2. 每个域的对应 encoderFunc;
  3. 序列化成Json时候的key值,如 student 内 name域将被预处理为 “name”: 。
  4. 是否是嵌套其他 struct 等特性。

fieldCache 正是缓存 Json 序列化过程中所需的结构体内部分成员属性?产?的。 它 也是 sync.Map 类型,其作?是为了缓存?些预处理的结果,在最终递归?成 json 字 符串时候使?。

序列化过程中,所需的稳定的结构体属性的如下 structFields 和 field 所示。我们 可以看到,有 nameBytes、nameNonEsc 等对应序列化过程中固定的属性或者值,都 可以被缓存下来,如 student ?的 Age 域,会被预处理为 ’”age”:‘,对每次序列化??,都稳定不变。

代码语言:javascript
复制
type structFields struct {
    list []field
    nameIndex map[string]int
}

// A field represents a single field found in a struct.
type field struct {
    name string
    nameBytes []byte // []byte(name)
    equalFold func(s, t []byte) bool // bytes.EqualFold or
equivalent

    nameNonEsc string // `"` + name + `":`
    nameEscHTML string // `"` + HTMLEscape(name) + `":`
    
    tag        bool
    index      []int
    typ        reflect.Type
    omitEmpty  bool
    quoted     bool

    encoder encoderFunc
}

为了性能考虑,序列化过程,缓存起来 struct ?每个 filed 对应的值,避免重复处理。同样为了提?速度,对结构体以上稳定属性进?预处理,且存储下来。?且,json 解析过程中,会存在结构体 array/map 等?量重复使?同?结构体的情况,该措施可以有效提?性能。

结构体的处理?较复杂,其中 typeFields 不仅仅只是列出 field 的事情要多,?且还做了?定的预处理,如对每个字段?成其 name 值,并存储:

  1. 对结构体内的域,进??度优先遍历,识别出所有需要解析的字段。
  2. 去除所有omitempty 且值为零值的字段,或者 tag 内有 ”-“ 字段。
  3. 遍历json tag,对每个字段?成其name值,如这样的字符串 "name:"
  4. 如果 bool int float 对应的 tag 是string ,则进?处理,标识需要添加 "
  5. 对匿名结构体、或者相同tag的结构体进?处理

下?结合源码,对其?度优先递归遍历 fields 的过程摘出进?分析。基本思想:原始 结构体定义为顶点 v,访问它的所有 field1, field1,..., fieldn,然后再依次访问 field1, field1 field2,…, fieldn 的域,直到所有与 v 有相通路径的所有顶点都被访问完为?。为 了避免结构体的引?关系可能会形成有向图状结构,遍历过程中使?了 visited 避免嵌 套进?。具体代码参? typeFields 函数,先结构体内结构体的所有域,压?fields 内。然后对每个 filed 递归调?typeEncoder,获取所有的 encoderFunc。

代码语言:javascript
复制
func typeFields(t reflect.Type) structFields {
    // Anonymous fields to explore at the current level and the
next.
    current := []field{}
    next := []field{{typ: t}}
    
    // Types already visited at an earlier level.
    visited := map[reflect.Type]bool{}
    
    // Fields found.
    var fields []field
    
        for _, f := range current {
            if visited[f.typ] {
                continue
            }
            visited[f.typ] = true
            
            // Scan f.typ for fields to include.
            for i := 0; i < f.typ.NumField(); i++ {
                sf := f.typ.Field(i)
                // Record found field and index sequence.
                ?成 field
                field.nameNonEsc = `"` + field.name + `":`
                fields = append(fields, field)
                continue
            }
         }
     for i := range fields {
        f := &fields[i]
        f.encoder = typeEncoder(typeByIndex(t, f.index))
     }
     
     return structFields{fields, nameIndex}
}

总结

本?剖析了 json 序列化的源码,并对?些关键细节进?分析。Json 序列化的源码使 ?反射来处理各种不同类型之间的通?逻辑,并通过递归来简化代码逻辑。

Json 序列化的源码同样也展示了在有?性能要求的代码的过程中,缓存的重要 性,需要尽可能多的缓存中间结果。这些细节,对于我们写?些基础的组件,有?定 的借鉴意义。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/o3kRK3ExvV9ECpR0KUsi
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com