今天小编给大家分享一下Golang中由零值和gob库特性引起BUG怎么解决的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

1 gob 与零值

gob

1.1 零值

零值是 Go 语言中的一个特性,简单说就是:Go 语言会给一些没有被赋值的变量提供一个默认值。譬如下面这段代码:

package main
import (
    "fmt"
)
type person struct {
    name   string
    gender int
    age    int
}
func main() {
    p := person{}
    var list []byte
    var f float32
    var s string
    var m map[string]int
    fmt.Println(list, f, s, m)
    fmt.Printf("%+v", p)
}
/* 结果输出
[] 0  map[]
{name: gender:0 age:0}
*/

零值在很多时候确实为开发者带来了方便,但也有许多不喜欢它的人认为零值的存在使得代码从语法层面上不严谨,带来了一些不确定性。譬如我即将在后文中详细描述的问题。

1.2 gob

gobencoding/gobgobgo binarygob
gobpickle
gobgobExample

2 问题

2.1 需求

在本文的开头,我简单叙述了问题的起源,这里我用一个更简单的模型来展开描述。

person
type person struct {
    // 和 json 库一样,字段首字母必须大写(公有)才能序列化
    ID     int
    Name   string // 姓名
    Gender int    // 性别:男 1,女 0
    Age    int    // 年龄
}
persongob

接着,我们有这样一个需求:

gob

2.2 代码

package, import, init()
  • defines.go

// .gob 文件所在目录
const DIR = "./persons"
type person struct {
    // 和 json 库一样,字段首字母必须大写(公有)才能序列化
    ID     int
    Name   string // 姓名
    Gender int    // 性别:男 1,女 0
    Age    int    // 年龄
}
// 需要持久化的对象们
var persons = []person{
    {0, "Mia", 0, 21},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
}
  • serializer.go

// serialize 将 person 对象序列化后存储到文件,
// 文件名为 ./persons/${p.id}.gob
func serialize(p person) {
    filename := filepath.Join(DIR, fmt.Sprintf("%d.gob", p.ID))
    buffer := new(bytes.Buffer)
    encoder := gob.NewEncoder(buffer)
    _ = encoder.Encode(p)
    _ = ioutil.WriteFile(filename, buffer.Bytes(), 0644)
}
// unserialize 将 .gob 文件反序列化后存入指针参数
func unserialize(path string, p *person) {
    raw, _ := ioutil.ReadFile(path)
    buffer := bytes.NewBuffer(raw)
    decoder := gob.NewDecoder(buffer)
    _ = decoder.Decode(p)
}
  • main.go

func main() {
    storePersons()
    countGender()
}
func storePersons() {
    for _, p := range persons {
        serialize(p)
    }
}
func countGender() {
    counter := make(map[int]int)
    // 用一个临时指针去作为文件中对象的载体,以节省新建对象的开销。
    tmpP := &person{}
    for _, p := range persons {
        // 方便起见,这里直接遍历 persons ,但只取 ID 用于读文件
        id := p.ID
        filename := filepath.Join(DIR, fmt.Sprintf("%d.gob", id))
        // 反序列化对象到 tmpP 中
        unserialize(filename, tmpP)
        // 统计性别
        counter[tmpP.Gender]++
    }
    fmt.Printf("Female: %+v, Male: %+v\n", counter[0], counter[1])
}

执行代码后,我们得到了这样的结果:

// 对象们
var persons = []person{
    {0, "Mia", 0, 21},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
}
// 结果输出
Female: 1, Male: 4
BUG

2.3 定位

countGender()forperson
// 添加行
fmt.Printf("%+v\n", tmpP)
// 结果输出
&{ID:0 Name:Mia Gender:0 Age:21}
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
&{ID:3 Name:Jenny Gender:1 Age:16}
&{ID:4 Name:Marry Gender:1 Age:30}
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 规律

由于上文中出问题的两个数据都是女性,程序员的直觉告诉我这也许并不是巧合。于是我尝试修改数据的顺序,将男女完全分开,然后进行测试:

// 第一组,先女后男
var persons = []person{
    {0, "Mia", 0, 21},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
}
// 结果输出
&{ID:0 Name:Mia Gender:0 Age:21}
&{ID:3 Name:Jenny Gender:0 Age:16}
&{ID:4 Name:Marry Gender:0 Age:30}
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
// 第二组,先男后女
var persons = []person{
    {1, "Jim", 1, 18},
    {2, "Bob", 1, 25},
    {0, "Mia", 0, 21},
    {3, "Jenny", 0, 16},
    {4, "Marry", 0, 30},
}
// 结果输出
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
&{ID:2 Name:Mia Gender:1 Age:21}
&{ID:3 Name:Jenny Gender:1 Age:16}
&{ID:4 Name:Marry Gender:1 Age:30}

吊诡的现象出现了,先女后男时,结果一切正常;先男后女时,男性正常,女性全都不正常,甚至 Mia 原本为 0 的 ID 这里也变成了 2!

经过反复地测试和对结果集的观察,我们得到了这样一个有规律的结论:所有男性数据都正常,出问题的全是女性数据!

进一步公式化描述这个结论就是:如果前面的数据为非 0 数字,同时后面的数据数字为 0 时,则后面的 0 会被它前面的非 0 所覆盖

3 答案

再次审计程序代码,我注意到了这一句:

// 用一个临时指针去作为文件中对象的载体,以节省新建对象的开销。
tmpP := &person{}

为了节省额外的新建对象的开销,我用了同一个变量来循环加载文件中的数据,并进行性别判定。结合前面我们发现的 BUG 规律,答案似乎近在眼前了:所谓后面的数据 0 被前面的非 0 覆盖,很可能是因为使用了同一个对象加载文件,导致前面的数据残留

for

我们修改一下代码(省略了多余部分):

for _, p := range persons {
    // ...
    tmpP := &person{}
    // ...
}
// 结果输出
&{ID:0 Name:Mia Gender:0 Age:21}
&{ID:1 Name:Jim Gender:1 Age:18}
&{ID:2 Name:Bob Gender:1 Age:25}
&{ID:3 Name:Jenny Gender:0 Age:16}
&{ID:4 Name:Marry Gender:0 Age:30}
Female: 3, Male: 2

对了!

结果确实如我们推想,是数据残留的原因。但这里又有一个问题了:为什么先 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

解决方法也很简单,就是我上面做的,不要使用公共对象去加载就好了。

以上就是“Golang中由零值和gob库特性引起BUG怎么解决”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注本站行业资讯频道。