欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > Go语言设计与实现 学习笔记 第九章 标准库

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

2024/10/25 10:21:57 来源:https://blog.csdn.net/tus00000/article/details/142471581  浏览:    关键词: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的序列化和反序列化结果:

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

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

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,标签名和字段名会建立一一对应的关系,后面的标签选项也会影响编解码的过程:

type Author struct {Name string `json:"name,omitempty"`Age  int32  `json:"age,string,omitempty"`
}

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

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标准库序列化,为了提供如此复杂和通用的功能,在静态语言中使用反射是常见的选项,我们了解一下该方法的实现:

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编码器;

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

// 为指定类型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函数,用于编码类型treturn 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等基本类型编码器和数组、结构体、切片等复杂类型的编码器:

func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {...switch t.Kind() {case reflect.Bool:return boolEncodercase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:return intEncodercase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:return uintEncodercase reflect.Float32:return float32Encodercase reflect.Float64:return float64Encodercase reflect.String:return stringEncodercase reflect.Interface:return interfaceEncodercase 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编码器,它的实现很简单,甚至没有太多值得介绍的地方:

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方法:

func newStructEncoder(t reflect.Type) encoderFunc {se := structEncoder{fields: cachedTypeFields(t)}return se.encode
}

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

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,类似cppfor _, 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语言的表达能力比较有限,反序列化的使用相对繁琐,需要传入一个变量帮助标准库进行反序列化:

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开始反序列化了:

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,将结果存入rverr := d.value(rv)// 如果解析出错if err != nil {// 添加错误上下文信息,然后返回return d.addErrorContext(err)}// 如果没有出错,该返回值为nilreturn d.savedError
}

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

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函数查找当前类型对应的非指针类型:

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解析对应的值:

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.Valuevar 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,将结果存入subvif err := d.value(subv); err != nil {return err}// 检查操作码是否是扫描结束,如果是,说明完成了对象扫描if d.opcode == scanEndObject {break}}return nil
}

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

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() {// 将字面量保存到vif err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {return err}}}return nil
}

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

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': // truei += len("rue")case 'f': // falsei += len("alse")case 'n': // nulli += 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库的实现,我们也可以从中学习到使用反射的各种方法。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com