使用 Go 语言时没有关注值传递和误用 for 循环导致的 bug

我们的业务代码中习惯使用 Map 维护一些 LocalCache,前两天发现自己维护的一个 LocalCache 数据有些不对:Cache 的 Key 为某个对象的ID,值为这个ID对应的 PO(即数据库中的对象),调试时发现所有的 Key 对应的值都是一样的,这是因为自己对一些细节没有关注到,还把 Java 那套东西搬来用导致的问题。

为了简化,我就不把业务代码搬上来了,写个简单的示例:

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
package main

import (
"encoding/json"
"fmt"
)

type Student struct {
ID int
Name string
Age int
}

func main() {

var students []Student
students = append(students, Student{
ID: 1,
Name: "张三",
Age: 18,
})
students = append(students, Student{
ID: 2,
Name: "李四",
Age: 19,
})
students = append(students, Student{
ID: 3,
Name: "王五",
Age: 20,
})

studentMap := make(map[int]*Student, len(students))
for _, student := range students {
studentMap[student.ID] = &student
}

bs, _ := json.Marshal(studentMap)
fmt.Println(string(bs))

}

上边代码输出如下:

1
{"1":{"ID":3,"Name":"王五","Age":20},"2":{"ID":3,"Name":"王五","Age":20},"3":{"ID":3,"Name":"王五","Age":20}}

可以看到所有的 value 是同一个 Student,为什么会出现这样的问题呢?因为 students 存储的是 Student 的值,在给 for 循环中的 student 赋值时,是复制了一个新的值给它,而 for 循环中的 student 变量所指向的地址是不变的。

可以打印 student 的地址看一下:

1
2
3
4
for _, student := range students {
fmt.Printf("%p \n", &student)
studentMap[student.ID] = &student
}

输出为:

1
2
3
4
0xc0000a6040 
0xc0000a6040
0xc0000a6040
{"1":{"ID":3,"Name":"王五","Age":20},"2":{"ID":3,"Name":"王五","Age":20},"3":{"ID":3,"Name":"王五","Age":20}}

这种情况下我们应该用 students 中索引对应数据的指针,上边 for 循环修改如下:

1
2
3
4
for i, student := range students {
fmt.Printf("%p \n", &students[i])
studentMap[student.ID] = &students[i]
}

输出为:

1
2
3
4
0xc0000b8000 
0xc0000b8020
0xc0000b8040
{"1":{"ID":1,"Name":"张三","Age":18},"2":{"ID":2,"Name":"李四","Age":19},"3":{"ID":3,"Name":"王五","Age":20}}

上边的情况给 student 赋值也是有问题的:

1
2
3
4
5
6
for _, student := range students {
student.Name = "test"
}

bs, _ := json.Marshal(students)
fmt.Println(string(bs))

输出:

1
[{"ID":1,"Name":"张三","Age":18},{"ID":2,"Name":"李四","Age":19},{"ID":3,"Name":"王五","Age":20}]

Java 写习惯了就以为迭代时的 student 指向的是 students 中的地址。