golang 预置 json 包的值覆盖测试

json 作为一种可读性高、跨平台的序列化手段,常用在持久化和网络间传输。一般情况下,只需考虑是否按照作者的意图序列化和反序列化;反序列化的目标一般是一个 空白的对象,供写入得到的值。

但有一些特殊情况,还要考虑反序列化过程中,值的覆盖性:用到的字段非常多,给每个都赋值很麻烦,所以提供一套默认值(注意默认值不一定是 0 值),只要 json 中没有指定,就转而使用默认值。这在 jQuery 中只需要使用 $.extend(default, opts1, opts2...);而如果想递归合并,则只需要把 true 作为第一个参数。

这一般出现在用 json 写配置文件,还有传递运行参数时,要求调用方即使默认值也要写上非常麻烦;而通过检测读到的是否 0 值再设置默认值则更麻烦,而且不合理(因为没法区分缺省值还是设置值,在 go 中除非全部使用指针)。所以,如果可以以初始化了默认值的对象作为反序列化的目标,将 json 中有指定的值覆盖上去,就是最好的选择。

json 在 go 中只是字符串,不像在 js 中有原生身份可以直接合并,所以在 Unmarshal 时能顺便进行覆盖是最好的了。前面我做了一些简单的测试,发现在 go 中这个思路可行,所以就这样实现了;但随着实现深入,特别是我为调用方编写文档时,我才意识到准确的覆盖对应的值,并不简单。(指定的值有覆盖,不指定的值保留默认,slice 和 map 的 value 可以准确覆盖到正确的 index / key)

话不多说,直接先跑,然后看输出结合源码分析:

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main
import (
"encoding/json"
//json "github.com/json-iterator/go"
"fmt"
)
type Inner struct {
A int
B float64
C bool
}
type Mid struct {
I Inner
Ip *Inner
D int
}
type Outer struct {
Inner
M Mid
Mp *Mid
Mids map[int]*Mid
Ins []*Inner
}
func main() {
o := &Outer {
Inner: Inner {
A: 111, // 按同名 field 覆盖,不影响其他 field
B: 222.22,
C: true,
},
M: Mid {
I: Inner {
A: 111, // 嵌套的 struct 也是按 field 覆盖
C: true,
},
},
Mp: &Mid {
I: Inner {
A: 111, // 指针指向的 struct 内部 field 同理
C: true,
},
},
Mids: map[int]*Mid {
0: &Mid{ // map elem 被整体覆盖
I: Inner {
C: true, // 被覆盖为默认值
},
Ip: &Inner {
B: 333, // 被覆盖为默认值
},
D: 444, // 被覆盖为默认值
},
1: &Mid{ // 但是不影响 map 的其他 elem,值被完整保留
I: Inner {
A: 555,
},
},
},
Ins: []*Inner {
&Inner { // slice elem 内部被按 field 覆盖
A: 666,
B: 77.7,
},
&Inner { // 同上
A: 888,
B: 99.9,
},
&Inner { // 但其他 elem 反而丢失了
A: 1010,
B: 11.11,
},
},
}
b, _ := json.Marshal(o)
fmt.Println("baseline:", string(b))
bv := []byte(`
{
"A": 123,
"M": {
"I": {
"B": 222
}
},
"Mp": {
"I": {
"B": 222
}
},
"Mids": {
"0": {
"I": {
"A": 234
},
"Ip": {
"A": 777
}
}
},
"Ins": [
{
"A": 456
},
{
"B": 765
}
]
}
`)
json.Unmarshal(bv, o)
b, _ = json.Marshal(o)
fmt.Println("overlay:", string(b))
}

结果

(重点看在 json string 里没有指定的值是否会被覆盖,以及 slice 、map 里其他成员的情况。为了更方便地看,我把对比的结果用注释写在了测试代码里。)

1
2
baseline: {"A":111,"B":222.22,"C":true,"M":{"I":{"A":111,"B":0,"C":true},"Ip":null,"D":0},"Mp":{"I":{"A":111,"B":0,"C":true},"Ip":null,"D":0},"Mids":{"0":{"I":{"A":0,"B":0,"C":true},"Ip":{"A":0,"B":333,"C":false},"D":444},"1":{"I":{"A":555,"B":0,"C":false},"Ip":null,"D":0}},"Ins":[{"A":666,"B":77.7,"C":false},{"A":888,"B":99.9,"C":false},{"A":1010,"B":11.11,"C":false}]}
overlay: {"A":123,"B":222.22,"C":true,"M":{"I":{"A":111,"B":222,"C":true},"Ip":null,"D":0},"Mp":{"I":{"A":111,"B":222,"C":true},"Ip":null,"D":0},"Mids":{"0":{"I":{"A":234,"B":0,"C":false},"Ip":{"A":777,"B":0,"C":false},"D":0},"1":{"I":{"A":555,"B":0,"C":false},"Ip":null,"D":0}},"Ins":[{"A":456,"B":77.7,"C":false},{"A":888,"B":765,"C":false}]}

结合源码分析

下面结合 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,没有实际测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// array consumes an array from d.data[d.off-1:], decoding into the value v.
// the first byte of the array ('[') has been read already.
func (d *decodeState) array(v reflect.Value) {
// ...
// ...
// Get element of array, growing if necessary.
if v.Kind() == reflect.Slice {
// Grow slice if necessary
if i >= v.Cap() {
newcap := v.Cap() + v.Cap()/2
if newcap < 4 {
newcap = 4
}
newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
reflect.Copy(newv, v)
v.Set(newv)
}
if i >= v.Len() {
v.SetLen(i + 1)
}
}
if i < v.Len() {
// Decode into element.
d.value(v.Index(i))
} else {
// Ran out of fixed array: skip.
d.value(reflect.Value{})
}
i++
// ...
// ...

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,所以它们的代码是在一起的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// object consumes an object from d.data[d.off-1:], decoding into the value v.
// the first byte ('{') of the object has been read already.
func (d *decodeState) object(v reflect.Value) {
// ...
// ...
var mapElem reflect.Value
for {
// ...
// ...
// Figure out field corresponding to key.
var subv reflect.Value
destring := false // whether the value is wrapped in a string to be decoded first
if v.Kind() == reflect.Map {
elemType := v.Type().Elem()
if !mapElem.IsValid() {
mapElem = reflect.New(elemType).Elem()
} else {
mapElem.Set(reflect.Zero(elemType))
}
// 如果是 map,subv 指向一个新初始化的 elem
subv = mapElem
} else {
// ...
// ...
subv = v
// ...
// ...
// 如果是 struct,则得到对应 field 的指针
subv = subv.Field(i)
// ...
// ...
}
// ...
// ...
// 解析下一层并把内容放到 subv,如果是 map,subv 指向一个空白的对象
{
d.value(subv)
}
// Write value back to map;
// if using struct, subv points into struct already.
if v.Kind() == reflect.Map {
// ...
// ...
// 把 subv 设置到对应的 key,该 key 对应的原有的数据全部丢失
// struct 没有这步,因为 subv 直接就是指向 field 的指针
v.SetMapIndex(kv, subv)
}
// ...
// ...

为什么 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 是直接覆盖,不能合并。

流行的第三方库:jsoniter

再看当下比较流行的第三方库 jsoniter 。同样的代码,只是单纯的把 "encoding/json" 换成 json "github.com/json-iterator/go" ,再看结果,是完全一样的,说明在覆盖性的兼容上,jsoniter 是兼容官方库的:

1
2
baseline: {"A":111,"B":222.22,"C":true,"M":{"I":{"A":111,"B":0,"C":true},"Ip":null,"D":0},"Mp":{"I":{"A":111,"B":0,"C":true},"Ip":null,"D":0},"Mids":{"0":{"I":{"A":0,"B":0,"C":true},"Ip":{"A":0,"B":333,"C":false},"D":444},"1":{"I":{"A":555,"B":0,"C":false},"Ip":null,"D":0}},"Ins":[{"A":666,"B":77.7,"C":false},{"A":888,"B":99.9,"C":false},{"A":1010,"B":11.11,"C":false}]}
overlay: {"A":123,"B":222.22,"C":true,"M":{"I":{"A":111,"B":222,"C":true},"Ip":null,"D":0},"Mp":{"I":{"A":111,"B":222,"C":true},"Ip":null,"D":0},"Mids":{"0":{"I":{"A":234,"B":0,"C":false},"Ip":{"A":777,"B":0,"C":false},"D":0},"1":{"I":{"A":555,"B":0,"C":false},"Ip":null,"D":0}},"Ins":[{"A":456,"B":77.7,"C":false},{"A":888,"B":765,"C":false}]}

知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
详情请点击查看协议具体内容。