json 作为一种可读性高、跨平台的序列化手段,常用在持久化和网络间传输。一般情况下,只需考虑是否按照作者的意图序列化和反序列化;反序列化的目标一般是一个 空白的对象,供写入得到的值。
但有一些特殊情况,还要考虑反序列化过程中,值的覆盖性:用到的字段非常多,给每个都赋值很麻烦,所以提供一套默认值(注意默认值不一定是 0 值),只要 json 中没有指定,就转而使用默认值。这在 jQuery 中只需要使用 $.extend(default, opts1, opts2...)
;而如果想递归合并,则只需要把 true
作为第一个参数。
这一般出现在用 json 写配置文件,还有传递运行参数时,要求调用方即使默认值也要写上非常麻烦;而通过检测读到的是否 0 值再设置默认值则更麻烦,而且不合理(因为没法区分缺省值还是设置值,在 go 中除非全部使用指针)。所以,如果可以以初始化了默认值的对象作为反序列化的目标,将 json 中有指定的值覆盖上去,就是最好的选择。
json 在 go 中只是字符串,不像在 js 中有原生身份可以直接合并,所以在 Unmarshal 时能顺便进行覆盖是最好的了。前面我做了一些简单的测试,发现在 go 中这个思路可行,所以就这样实现了;但随着实现深入,特别是我为调用方编写文档时,我才意识到准确的覆盖对应的值,并不简单。(指定的值有覆盖,不指定的值保留默认,slice 和 map 的 value 可以准确覆盖到正确的 index / key)
话不多说,直接先跑,然后看输出结合源码分析:
测试代码
|
|
结果
(重点看在 json string 里没有指定的值是否会被覆盖,以及 slice 、map 里其他成员的情况。为了更方便地看,我把对比的结果用注释写在了测试代码里。)
|
|
结合源码分析
下面结合 encoding/json/decode.go
的源码,大概梳理一下结论:
1. struct 内的基本类型 field
(struct 对应 json 中的 object,以 {} 包裹;基本类型对应 json 中的 字面量 literal,内容是一个数字或者字符串)
值按同名覆盖,没有指定的的 field 保留原值;这条即使是 嵌套的 struct 内部的 field 也成立,包括匿名嵌套,还有指针指向的 struct 的 field。
2. slice 类型
(对应 json 中的 array,以 [] 包裹)
slice elem 按 index 一一对应, struct 内部 field 同规则 1;但是 slice 会调整长度跟 json 保持一致: json 多出的会添加,反之原 slice 多出的会因为长度调整被丢弃。从源码看,如果是 array,因为长度无法调整,json 数量比较少的话不会丢弃原 array 的内容;反之 json 多出的内容会被丢弃。不过很少使用 array,没有实际测试。
|
|
3. map 类型
(对应 json 中的 object,以 {} 包裹)
json 中指定的 key 会整个 elem 被覆盖;没有指定的 key 则完整保留。
其中最主要的差别是 struct filed 和 map elem 之间:虽然在 json 中同样以 object 表示,但 struct 设置时,获得的是 field 的 pointer,如果 field 本身也是一个 struct,那么就会递归处理,一直到最里层的基本类型才被覆盖;而 map elem 是直接覆盖, 如果这个 map elem 本身是一个 struct,那么它原有的数据就会丢失 。
由于 map 和 struct 都对应 object,所以它们的代码是在一起的:
|
|
为什么 slice 可以直接 d.value(v.Index(i))
把对应的 elem 递归交给下一层去合并, 而 map 却不能直接 d.value(v.MapIndex(key))
,而是在这一层直接把 elem 给覆盖掉了? 是因为什么原因做不到,还是出于什么考虑不这样实现?时间关系,我还没找到答案,这里先留一个问号。
初步结论
为了达到『指定值覆盖,缺省值保留』的效果:
- 如果需要嵌套 struct,内层 struct 最好作为一个 field 存在,指针还是对象都可以;
- 如果是一组确定数量的 struct,最好以 array 的方式定义,并且确保 index 正确;缺省的 struct 传参时可以用空 objecct
{}
占位以确保顺序对应; - 如果是一组数量不确定的 struct,则要权衡 slice 和 map 的利弊:
- slice 内的 struct 可以正常合并,但是 slice 比 json 多出的 elem 会被丢弃;如果 json 的数量比默认数量少,则不仅中间的缺省 struct 要用
{}
占位,后续缺省的 struct 也得占位——这需要默认值的数量是确定的; - map 刚好反过来,没有指定的 key 也不会被删除,但是指定了的 key 对应的 value 是直接覆盖,不能合并。
- slice 内的 struct 可以正常合并,但是 slice 比 json 多出的 elem 会被丢弃;如果 json 的数量比默认数量少,则不仅中间的缺省 struct 要用
流行的第三方库:jsoniter
再看当下比较流行的第三方库 jsoniter 。同样的代码,只是单纯的把 "encoding/json"
换成 json "github.com/json-iterator/go"
,再看结果,是完全一样的,说明在覆盖性的兼容上,jsoniter 是兼容官方库的:
|
|
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
详情请点击查看协议具体内容。