起源
就在今年9月份,我负责的部门平台项目发布了一个新版本,该版本同时上线了一个新功能,简单说有点类似定时任务。头一天一切正常,但第二天出现了极少数任务没有正常执行(已经暂停的任务继续执行,正常的任务反而没有执行)的情况。
gobgo
后文中我会用一个更简化的例子描述这个 BUG。
1 gob 与零值
gob
1.1 零值
零值是 Go 语言中的一个特性,简单说就是:Go 语言会给一些没有被赋值的变量提供一个默认值。譬如下面这段代码:
零值在很多时候确实为开发者带来了方便,但也有许多不喜欢它的人认为零值的存在使得代码从语法层面上不严谨,带来了一些不确定性。譬如我即将在后文中详细描述的问题。
1.2 gob
gobencoding/gobgobgo binarygob
gobpickle
gobgobExample
2 问题
2.1 需求
在本文的开头,我简单叙述了问题的起源,这里我用一个更简单的模型来展开描述。
person
persongob
接着,我们有这样一个需求:
gob
2.2 代码
package, import, init()
- defines.go
- serializer.go
- main.go
执行代码后,我们得到了这样的结果:
BUG
2.3 定位
countGender()forperson
Gender
gobxxd
>$ xxd persons/1.gob
0000000: 37ff 8103 0101 0670 6572 736f 6e01 ff82 7......person...
0000010: 0001 0401 0249 4401 0400 0104 4e61 6d65 .....ID.....Name
0000020: 010c 0001 0647 656e 6465 7201 0400 0103 .....Gender.....
0000030: 4167 6501 0400 0000 0eff 8201 0201 034a Age............J
0000040: 696d 0102 0124 00 im...$.
>$ xxd persons/0.gob
0000000: 37ff 8103 0101 0670 6572 736f 6e01 ff82 7......person...
0000010: 0001 0401 0249 4401 0400 0104 4e61 6d65 .....ID.....Name
0000020: 010c 0001 0647 656e 6465 7201 0400 0103 .....Gender.....
0000030: 4167 6501 0400 0000 0aff 8202 034d 6961 Age..........Mia
0000040: 022a 00 .*.
gob
2.4 规律
由于上文中出问题的两个数据都是女性,程序员的直觉告诉我这也许并不是巧合。于是我尝试修改数据的顺序,将男女完全分开,然后进行测试:
吊诡的现象出现了,先女后男时,结果一切正常;先男后女时,男性正常,女性全都不正常,甚至 Mia 原本为 0 的 ID 这里也变成了 2!
经过反复地测试和对结果集的观察,我们得到了这样一个有规律的结论:所有男性数据都正常,出问题的全是女性数据!
进一步公式化描述这个结论就是:如果前面的数据为非 0 数字,同时后面的数据数字为 0 时,则后面的 0 会被它前面的非 0 所覆盖。
3 答案
再次审计程序代码,我注意到了这一句:
为了节省额外的新建对象的开销,我用了同一个变量来循环加载文件中的数据,并进行性别判定。结合前面我们发现的 BUG 规律,答案似乎近在眼前了:所谓后面的数据 0 被前面的非 0 覆盖,很可能是因为使用了同一个对象加载文件,导致前面的数据残留。
for
我们修改一下代码(省略了多余部分):
对了!
结果确实如我们推想,是数据残留的原因。但这里又有一个问题了:为什么先 0 后非 0 (先女后男)的情况下,老方法读取的数据又一切正常呢?以及,除了 0 会被影响外,其他的数字(年龄)又都不会被影响?
所有的问题现在似乎都在指向 0 这个特殊数字!
gob
If a field has the zero value for its type (except for arrays; see above), it is omitted from the transmission.
翻译一下:
如果一个字段的类型拥有零值(数组除外),它会在传输中被省略。
structfield
根据我们前面得到的结论,以及官方文档的说明,我们现在终于可以得出一个完整的结论了:
gob
解决方法也很简单,就是我上面做的,不要使用公共对象去加载就好了。
4 回顾
person.Gender
gob