Go语言设计与实现 学习笔记 第九章 标准库

9.1 JSON

JSON(JavaScript对象表示,JavaScript Object Notation)作为一种轻量级的数据交换格式,在今天几乎占据了绝大多数的市场份额。虽然与更紧凑的数据交换格式相比,它的序列化和反序列化性能不足,但是它也提供了良好的可读性与易用性,在不追求极致性能的情况下,JSON是一种非常好的选择。

9.1.1 设计原理

几乎所有的现代编程语言都会将处理JSON的函数直接纳入标准库,Go语言也不例外,它通过encoding/json对外提供标准的JSON序列化和反序列化方法,即encoding/json.Marshalencoding/json.Unmarshal,它们也是包中最常用的两个方法。

序列化和反序列化的开销完全不同,JSON反序列化的开销是序列化开销的好几倍。Go语言中的JSON序列化过程不需要被序列化的对象预先实现任何接口,它会通过反射获取结构体或数组中的值并以树形的结构递归地进行编码,标准库也能根据encoding/json.Unmarshal中传入的值对JSON进行解码。

Go语言的JSON标准库编码和解码的过程大量地运用了反射特性,你会在本节的后半部分看到大量反射代码。我们在这里会简单介绍JSON标准库中的接口和标签,这是它为开发者提供的为数不多的影响编解码过程的接口。

接口

JSON标准库中提供了encoding/json.Marshalerencoding/json.Unmarshaler两个接口,分别可以影响JSON的序列化和反序列化结果:

go 复制代码
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

在JSON序列化和反序列化的过程中,会使用反射判断结构体类型是否实现了上述接口,如果实现了上述接口就会优先使用对应的方法进行编码和解码操作,除了这两个方法之外,Go语言其实还提供了另外两个用于控制编解码结果的方法,即encoding.TextMarshalerencoding.TextUnmarshaler

go 复制代码
type TextMarshaler interface {
    MarshalText() (text []byte, err error)
}

type TextUnmarshaler interface {
    UnmarshalText(text []byte) error
}

一旦发现JSON相关的序列化方法没有被实现,上述两个方法会作为候选方法被JSON标准库调用,参与编解码的过程。总的来说,我们可以在任意类型上实现上述四个方法自定义最终的结果,后面两个方法的适用范围更广,但不会被JSON标准库优先调用。

标签

默认情况下,当我们在序列化和反序列化结构体时,标准库都会认为字段名和JSON中的键具有一一对应的关系,然而Go语言的字段一般都是驼峰命名法,JSON中下划线的命名方式相对比较常见,使用标签特性可以建立键与字段之间的映射关系。

JSON中的标签由两部分组成,如下所示的nameage都是标签名,后面的字符串是标签选项,即encoding/json.tagOptions,标签名和字段名会建立一一对应的关系,后面的标签选项也会影响编解码的过程:

go 复制代码
type Author struct {
    Name string `json:"name,omitempty"`
    Age  int32  `json:"age,string,omitempty"`
}

常见的两个标签是stringomitempty,前者表示当前的整数或浮点数由JSON中的字符串表示,而omitempty会在字段值为零值时,直接在生成的JSON中忽略对应的键值对,例如"age":0"author":""等。标准库会使用encoding/json.parseTag函数来解析标签:

go 复制代码
func parseTag(tag string) (string, tagOptions) {
    // 寻找逗号分隔符,如果找到逗号
    if idx := strings.Index(tag, ","); idx != -1 {
        // 第一个逗号前的内容是标签名,之后的部分是标签选项
        return tag[:idx], tagOptions(tag[idx+1:])
    }
    // 只有标签名,没有选项
    return tag, tagOptions("")
}

从该方法的实现中,我们能分析出JSON标准库中的合法标签是什么形式的------标签名和标签选项都以,连接,最前面的字符串为标签名,后面的都是标签选项。

9.1.2 序列化

encoding/json.Marshal是JSON标准库中提供的最简单的序列化函数,它会接收一个interface{}类型的值作为参数,这也意味着几乎全部的Go语言变量都可以被JSON标准库序列化,为了提供如此复杂和通用的功能,在静态语言中使用反射是常见的选项,我们了解一下该方法的实现:

go 复制代码
func Marshal(v interface{}) ([]byte, error) {
    // 获取一个编码状态,这个状态用于存储编码过程中的临时数据和状态
    e := newEncodeState()
    // 在编码状态e上调用marshal将v编码成JSON
    // escapeHTML字段设为true,表示转义HTML相关字符,防止跨站脚本攻击(XSS)
    // 一个XSS的例子是代码注入,比如用户评论<script>alert('你被攻击了!')</script>
    // 就会执行script标签中的JavaScript代码
    err := e.marshal(v, encOpts{escapeHTML: true})
    if err != nil {
        return nil, err
    }
    // 获取编码完成的JSON数据,将其append到一个新的切片中,确保返回的切片不会被后续操作修改
    buf := append([]byte(nil), e.Bytes()...)
    // 将编码状态放回池中
    encodeStatePool.Put(e)
    return buf, nil
}

上述方法会调用encoding/json.newEncodeState从全局的编码状态池中获取encoding/json.encodeState,随后的序列化过程都会使用这个编码状态,该结构体也会在编码结束后被重新放回池中以便重复利用。

按照上图所示的调用栈,一系列的序列化方法在最后获取了对象的反射类型,并调用了encoding/json.newTypeEncoder这个核心的编码方法,该方法会递归地为所有类型找到对应的编码方法,它的执行过程可分为以下两个步骤:

1.获取用户自定义的encoding/json.Marshalerencoding/TextMarshaler编码器;

2.获取标准库中为基本类型内置的JSON编码器;

在该方法的第一部分,会检查当前值的类型是否可以使用用户自定义的编码器,这里有两种不同的判断方法:

go 复制代码
// 为指定类型t获取编码函数
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
    // 如果t不是指针类型 && 允许获取地址 && *t实现了marshalerType接口
    if t.Kind() != reflect.Ptr && allowAddr && reflect.PtrTo(t).Implements(marshalerType) {
        // 创建一个条件编码器
        // addrMarshalerEncoder函数用于对实现了marshalerType接口的指针类型进行编码
        // 第二个参数递归地调用自身,但这次不允许获取地址,防止无限递归
        return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
    }
    // 如果类型t本身实现了marshalerType接口
    if t.Implements(marshalerType) {
        // 直接返回marshalerEncoder函数,用于编码类型t
        return marshalerEncoder
    }
    // 如果t不是指针类型 && 允许获取地址 && *t实现了textMarshalerType接口
    if t.Kind() != reflect.Ptr && allowAddr && reflect.PtrTo(t).Implements(textMarshalerType) {
        return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false))
    }
    // 如果类型t本身实现了textMarshalerEncoder接口
    if t.Implements(textMarshalerType) {
        return textMarshalerEncoder
    }
    ...
}

1.如果当前值是值类型(即非指针类型)、可以取地址、值类型对应的指针类型实现了encoding/json.Marshaler接口,调用encoding/json.newCondAddrEncoder获取一个条件编码器,条件编码器会在encoding/json.addrMarshalerEncoder失败时重新选择新的编码器;

2.如果当前类型实现了encoding/json.Marshaler接口,可以直接使用encoding/json.marshalerEncoder对该值进行序列化;

在这段代码中,标准库对encoding.Text.Marshaler的处理也几乎完全相同,只是它会先判断encoding/json.Marshaler接口,这也印证了我们在设计原理一节中的结论。

encoding/json.newTypeEncoder方法随后会根据传入值的反射类型获取对应的编码器,其中包括boolintfloat等基本类型编码器和数组、结构体、切片等复杂类型的编码器:

go 复制代码
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
    ...
    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.Ptr:
        return newPtrEncoder(t)
    default:
        return unsupportedTypeEncoder
    }
}

我们在这里就不一一介绍全部的内置类型编码器了,只挑选其中几个帮助读者了解整体的设计。首先我们来看布尔值的JSON编码器,它的实现很简单,甚至没有太多值得介绍的地方:

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

它会根据当前值向编码状态中写入不同的字符串,也就是truefalse,此外还会根据编码配置决定是否要在布尔值周围加上双引号,而其他基本类型编码器也是大同小异。

复杂类型的编码器有着相对复杂的控制结构,我们在这里以结构体的编码器encoding/json.structEncoder为例介绍它们的原理,encoding/json.newStructEncoder会为当前结构体的所有字段调用encoding/json.typeEncoder(从下面的代码来看,显然没有调用encoding/json.typeEncoder方法,而且该方法没有介绍过,但应该是获取并缓存特定类型对应的编码函数的函数)获取类型编码器并返回encoding/json.structEncoder.encode方法:

go 复制代码
func newStructEncoder(t reflect.Type) encoderFunc {
    se := structEncoder{fields: cachedTypeFields(t)}
    return se.encode
}

encoding/json.structEncoder.encode的实现我们能看出结构体序列的结果,该方法会遍历结构体中的全部字段,在写入了字段名后,它会调用字段对应类型的编码方法将该字段对应的JSON写入缓冲区:

go 复制代码
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
        // 遍历字段f.index,该字段用于支持嵌套结构体,它表示索引路径
        // 即从顶层结构体到内层结构体中字段的路径
        // 此处的i遮蔽了外层循环的i,类似cpp
        for _, i := range f.index {
            // 如果当前值是指针类型
            if fv.Kind() == reflect.Ptr {
                // 如果指针为空
                if fv.IsNil() {
                    // 遍历下一个字段
                    continue FieldLoop
                }
                // 获取指针指向的实际值
                fv = fv.Elem()
            }
            // 获取下一层结构体
            fv = fv.Field(i)
        }
        
        // 如果忽略空字段 && 字段为空
        if f.omitEmpty && isEmptyValue(fv) {
            continue
        }
        // 写入字段前的分隔符
        e.WriteByte(next)
        // 分隔符第一次是{,后面都是,
        next = ','
        // 写入字段名
        e.WriteString(f.nameNonEsc)
        // 字段值是否需要引号
        opts.quoted = f.quoted
        // 调用字段的编码函数进行编码
        f.encoder(e, fv, opts)
    }
    // 如果next仍为{,说明是空对象
    if next == '{' {
        e.WriteString("{}")
    // 否则,写入对象结尾的右花括号
    } else {
        e.WriteByte('}')
    }
}

数组以及指针等编码器的实现原理与该方法也没有太多区别,它们都会使用类似的策略递归地调用持有字段的编码方法,这就形成一个如下图所示的树形结构:

树形结构的所有叶节点都是基础类型编码器或开发者自定义的编码器,得到了整棵树的编码器后会调用encoding/json.encodeState.reflectValue从根节点依次调用整棵树的序列化函数,整个JSON序列化的过程其实是查找类型和子类型的编码方法并调用的过程,它利用了大量反射特性做到了足够通用。

9.1.3 反序列化

标准库会使用encoding/json.Unmarshal函数处理JSON的反序列化,与执行过程确定的序列化相比,反序列化的过程像一个逐渐探索的过程,所以会复杂很多,开销也会高出好几倍。因为Go语言的表达能力比较有限,反序列化的使用相对繁琐,需要传入一个变量帮助标准库进行反序列化:

go 复制代码
func Unmarshal(data []byte, v interface{}) error {
    var d decodeState
    // 检查输入的JSON串是否有效
    err := checkValid(data, &d.scan)
    if err != nil {
        return err
    }
    
    // 初始化解码状态
    d.init(data)
    return d.unmarshal(v)
}

在真正执行反序列化前,会先调用encoding/json.checkValid验证传入JSON的合法性保证在反序列化的过程中不会遇到语法问题,在通过合法性验证后,标准库就会初始化数据并调用encoding/json.decodeState.unmarshal开始反序列化了:

go 复制代码
func (d *decodeState) unmarshal(v interface{}) error {
    // 使用反射获取类型为reflect.Value的变量v的值
    rv := reflect.ValueOf(v)
    // 如果v不是指针类型 || v是空指针
    if rv.Kind() != reflect.Ptr || rv.IsNil {
        // 返回InvalidUnmarshalError错误,因为没有地方存储解码后的数据
        return &InvalidUnmarshalError{reflect.TypeOf(v)}
    }
    // 重置解码器的扫描状态
    d.scan.reset()
    // 跳过输入中的所有空白字符
    d.scanWhile(scanSkipSpace)
    // 解析JSON,将结果存入rv
    err := d.value(rv)
    // 如果解析出错
    if err != nil {
        // 添加错误上下文信息,然后返回
        return d.addErrorContext(err)
    }
    // 如果没有出错,该返回值为nil
    return d.savedError
}

如果传入的值不是指针或是空指针,当前方法就会返回我们经常会见到的错误encoding/json.InvalidUnmarshalError,使用格式化输出可以将该错误转换成json: Unmarshal(non-pointer xxx)。该方法调用的encoding/json.decodeState.value是所有反序列化过程的执行入口:

go 复制代码
func (d *decodeState) value(v reflect.Value) error {
    // 根据操作码决定如何解析
    switch d.opcode {
    default:
        panic(phasePanicMsg)
    case scanBeginArray:
        ...
    case scanBeginLiteral:
        ...
    // 如果要解析的是一个对象
    case scanBeginObject:
        // 如果提供的反射值有效
        if v.IsValid() {
            // 解析对象,将解析结果存入v,如果解析失败
            if err := d.object(v); err != nil {
                return err
            }
        // 如果反射值无效,跳过当前对象
        } else {
            d.skip()
        }
        // 更新解析器状态,为解析下一个值做准备
        d.scanNext()
    }
    return nil
}

该方法作为最顶层的反序列化方法可以接收三种不同类型的值,也就是数组、字面量、对象,这三种类型都可以作为JSON的顶层对象,我们首先来了解一下标准库是如何解析JSON中对象的,该过程会使用encoding/json.decodeState.object函数进行反序列化,它会先调用encoding/json.indirect函数查找当前类型对应的非指针类型:

go 复制代码
func (d *decodeState) object(v reflect.Value) error {
    // 递归地解引用指针,直到得到非指针的底层值
    u, ut, pv := indirect(v, false)
    // 如果u非nil,即实现了Unmarshaler接口
    if u != nil {
        // 获取当前读取的位置索引,记录解码开始位置
        start := d.readIndex()
        // 将索引移至对象的结束位置
        d.skip()
        // 调用目标类型实现的UnmarshalJSON方法
        return u.UnmarshalJSON(d.data[start:d.off])
    }
    ...
}

在调用encoding/json.indirect的过程中,如果当前值的类型是**Type,那么它会依次检查形如**Type*TypeType类型是否实现了encoding/json.Unmarshalencoding/json.Unmarshal是json库的解码接口,这里应该是Unmarshaler接口)或encoding.TextUnmarshaler接口;如果实现了该接口,标准库会直接调用UnmarshalJSON方法使用开发者定义的方法完成反序列化(根据以上代码,只调用了UnmarshalJSON)。

在其他情况下,仍会回到默认的逻辑中处理对象中的键值对,如下代码会调用encoding/json.decodeState.rescanLiteral方法扫描JSON中的键并在结构体中找到对应字段的反射值,接下来继续扫描符号:,并调用encoding/json.decodeState.value解析对应的值:

go 复制代码
func (d *decodeState) object(v reflect.Value) error {
    ...
    // 将目标值设为indirect方法返回的值
    v = pv
    // 获取目标值的类型
    t := v.Type()
    // 获取目标类型的字段信息
    fields = cachedTypeFields(t)
    // 循环处理每个键值对
    for {
        // 记录读取位置的起始索引
        start := d.readIndex()
        // 重新扫描当前字节序列,用于解析一个完整的JSON字面量(如键名)
        d.rescanLiteral()
        // 获取表示键名的字节序列
        item := d.data[start:d.readIndex()]
        // 去除引号等转义字符,将字节序列转换为实际的字符串键名
        key, _ := d.unquoteBytes(item)
        var subv reflect.Value
        var f *field
        // 查找键名对应的字段索引
        if i, ok := fields.nameIndex[string(key)]; ok {
            // 获取键名对应的字段信息
            f = &fields.list[i]
        }
        // 如果找到对应字段
        if f != nil {
            subv = v
            // 遍历字段的索引路径,用于处理嵌套结构体
            for _, i := range f.index {
                // 逐级获取对应字段值
                subv = subv.Field(i)
            }
        }
        
        // 确保当前操作是在解析一个对象键
        if d.opcode != scanObjectKey {
            panic(phasePanicMsg)
        }
        // 跳过JSON中的空白字符
        d.scanWhile(scanSkipSpace)
        
        // 解析JSON,将结果存入subv
        if err := d.value(subv); err != nil {
            return err
        }
        // 检查操作码是否是扫描结束,如果是,说明完成了对象扫描
        if d.opcode == scanEndObject {
            break
        }
    }
    return nil
}

当上述方法调用encoding/json.decodeState.value时,该方法会重新判断键对应的值是否是对象、数组、字面量,因为数组和对象都是集合类型,所以该方法会递归地进行扫描,在这里就不介绍集合类型的解析过程了,我们来简单分析一下字面量是如何被处理的:

go 复制代码
func (d *decodeState) value(v reflect.Value) error {
    switch d.opcode {
    default:
        panic(phasePanicMsg)
        
    case scanBeginArray:
        ...
    case scanBeginObject:
        ...
    case scanBeginLiteral:
        // 保存当前读取的索引位置
        start := d.readIndex()
        // 扫描JSON,获取一个字面量
        d.rescanLiteral()
        // 如果v有效
        if v.IsValid() {
            // 将字面量保存到v
            if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
                return err
            }
        }
    }
    return nil
}

字面量的扫描会通过encoding/json.decodeState.rescanLiteral,该方法会依次扫描缓冲区中的字符并根据字符的不同对字符串进行切片,整个过程有点像编译器的词法分析:

go 复制代码
func (d *decodeState) rescanLiteral() {
    // 获取当前解析的数据和当前解析的位置
    data, i := d.data, d.off
Switch:
    // 根据解析位置前一字节判断字面值类型
    switch data[i-1] {
    case '"': // string
        ...
    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': // number
        ...
    case 't': // true
        i += len("rue")
    case 'f': // false
        i += len("alse")
    case 'n': // null
        i += len("ull") 
    }
    // 如果更新后的i仍小于数据长度,意味着还未到达数据末尾
    if i < len(data) {
        // 根据当前i指向的索引设置新的操作码,用于决定如何处理接下来的值
        d.opcode = stateEndValue(&d.scan, data[i])
    } else {
        // 字面量扫描结束
        d.opcode = scanEnd
    }
    // 更新解析的偏移量
    d.off = i + 1
}

因为JSON中的字面量其实也只包含字符串、数字、布尔值、空值几种,所以该方法的实现也不会特别复杂,当该方法扫描完对应的字面量之后,我们就可以调用encoding/json.decodeState.literalStore将字面量存储到反射类型变量所在的地址中,在这个过程中会调用反射的reflect.Value.SetIntreflect.Value.SetFloatreflect.Value.SetBool等方法。

9.1.4 小结

JSON本身就是一种树形的数据结构,无论是序列化还是反序列化,都会遵循自顶向下的编码和解码过程,使用递归的方式处理JSON对象。作为标准库的JSON提供的接口非常简洁,虽然它的性能一直被开发者所诟病,但作为框架它提供了很好的通用性,通过分析JSON库的实现,我们也可以从中学习到使用反射的各种方法。

相关推荐
彤银浦1 小时前
python学习记录4
学习
寅双木1 小时前
如何选购笔记本电脑?要看哪些参数?
笔记·笔记本电脑·cpu·显卡·内存条·固态硬盘·机械硬盘
解孔明2 小时前
部署项目正常点击一切正常,刷新后变404
笔记
时空无限2 小时前
go 安装三方库
后端·golang
孞㐑¥2 小时前
C语言—顺序表(含通讯录项目)
c语言·开发语言·经验分享·笔记
百锦再2 小时前
学习Python的难点分析
java·python·学习
GoppViper2 小时前
golang 如何生成唯一的 UUID
开发语言·后端·golang·uuid
LearnTech_1234 小时前
【学习笔记】手写 Tomcat 四
java·笔记·学习·tomcat·手写 tomcat
!!!!!!!!!!!!!!!!.4 小时前
蓝队如何溯源追踪
笔记·安全·web安全