这篇文章是为了给自己烙印上正确使用切片的印记,并加深对切片地址的理解。原因是线上代码产生的结果不符合预期,排查下来,是因为对slice的理解不够,采用了错误的用法,导致bug出现,因此记录下来,并修复这个问题。
先上代码,以下是线上代码的简单还原,输出结果被分割成了三部分,以下用第一部分,第二部分,第三部分来命名各部分数据。
type SimpleStruct struct {
ID string
A int
B int
}
func TestUnexpectedFun(t *testing.T) {
var dataArr = []SimpleStruct{
{"a", 1, 2},
{"b", 3, 4},
{"c", 5, 6},
{"d", 7, 8},
}
t.Logf("ptr of dataArr %p", &dataArr)
for _, v := range dataArr {
t.Logf("id=%s, %p", v.ID, &v)
}
t.Log("====================================")
var dataMap = make(map[string]*SimpleStruct)
var wg sync.WaitGroup
var mu sync.RWMutex
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
t.Logf("ptr of dataArr %p", &dataArr)
for _, v := range dataArr {
t.Logf("id=%s, %p", v.ID, &v)
if _, ok := dataMap[v.ID]; !ok {
dataMap[v.ID] = &v
continue
}
dataMap[v.ID].A = v.A
dataMap[v.ID].B = v.B
}
mu.Unlock()
}()
wg.Wait()
t.Log("====================================")
t.Logf("ptr of dataMap %p", &dataMap)
for id, v := range dataMap {
t.Logf("id->%p:%+v", &v, v)
}
}
输出结果:
=== RUN TestUnexpectedFun
req_test.go:94: ptr of dataArr 0xc000004a50
req_test.go:96: id=a, 0xc000048a40
req_test.go:96: id=b, 0xc000048a40
req_test.go:96: id=c, 0xc000048a40
req_test.go:96: id=d, 0xc000048a40
req_test.go:99: ====================================
req_test.go:108: ptr of dataArr 0xc000004a50
req_test.go:110: id=a, 0xc000048b40
req_test.go:110: id=b, 0xc000048b40
req_test.go:110: id=c, 0xc000048b40
req_test.go:110: id=d, 0xc000048b40
req_test.go:122: ====================================
req_test.go:123: ptr of dataMap 0xc000006628
req_test.go:125: id:a->0xc000006630:&{ID:d A:7 B:8}
req_test.go:125: id:b->0xc000006630:&{ID:d A:7 B:8}
req_test.go:125: id:c->0xc000006630:&{ID:d A:7 B:8}
req_test.go:125: id:d->0xc000006630:&{ID:d A:7 B:8}
--- PASS: TestUnexpectedFun (0.00s)
PASS
从第三部分最终输出的结果来看,我们拿到的数据都是异常的,所有数据都赋值了id=d的数据。
这个问题的原因很好理解,是因为我们赋值给dataMap的数是dataArr里数据的地址,但是dataArr里每个元素的地址都一样,所以最后取到的数据都相同。这篇文章go语言关于切片类型内存地址的理解很好的解释了这个问题。
这个问题有两种解决办法: 1、改变源数据dataArr的数据类型,由 []SimpleStruct 改为 []*SimpleStruct ,然后修改dataMap的赋值方式,由 dataMap[v.ID] = &v 改为 dataMap[v.ID] = v ,输出结果如下,虽然各个元素的地址仍然一样,但是改变了dataMap的赋值方式,可以有效的解决该问题
=== RUN TestUnexpectedFun
req_test.go:94: ptr of dataArr 0xc00009ca38
req_test.go:96: id=a, 0xc0000c4618
req_test.go:96: id=b, 0xc0000c4618
req_test.go:96: id=c, 0xc0000c4618
req_test.go:96: id=d, 0xc0000c4618
req_test.go:99: ====================================
req_test.go:108: ptr of dataArr 0xc00009ca38
req_test.go:110: id=a, 0xc0000c4628
req_test.go:110: id=b, 0xc0000c4628
req_test.go:110: id=c, 0xc0000c4628
req_test.go:110: id=d, 0xc0000c4628
req_test.go:122: ====================================
req_test.go:123: ptr of dataMap 0xc0000c4620
req_test.go:125: id:a->0xc0000c4630:&{ID:a A:1 B:2}
req_test.go:125: id:b->0xc0000c4630:&{ID:b A:3 B:4}
req_test.go:125: id:c->0xc0000c4630:&{ID:c A:5 B:6}
req_test.go:125: id:d->0xc0000c4630:&{ID:d A:7 B:8}
--- PASS: TestUnexpectedFun (0.00s)
PASS
2、不改变源数据dataArr的数据类型,只改变dataMap的赋值方式,每个dataMap的元素都重新实例化,然后再赋值,代码如下
if _, ok := dataMap[v.ID]; !ok {
dataMap[v.ID] = &SimpleStruct{}
}
dataMap[v.ID].ID = v.ID
dataMap[v.ID].A = v.A
dataMap[v.ID].B = v.B
输出结果如下:
=== RUN TestUnexpectedFun
req_test.go:95: ptr of dataArr 0xc000004a50
req_test.go:97: id=a, 0xc000048a40
req_test.go:97: id=b, 0xc000048a40
req_test.go:97: id=c, 0xc000048a40
req_test.go:97: id=d, 0xc000048a40
req_test.go:97: id=b, 0xc000048a40
req_test.go:100: ====================================
req_test.go:109: ptr of dataArr 0xc000004a50
req_test.go:111: id=a, 0xc000048b60
req_test.go:111: id=b, 0xc000048b60
req_test.go:111: id=c, 0xc000048b60
req_test.go:111: id=d, 0xc000048b60
req_test.go:111: id=b, 0xc000048b60
req_test.go:123: ====================================
req_test.go:124: ptr of dataMap 0xc000006628
req_test.go:126: id:b->0xc000006630:&{ID:b A:4 B:5}
req_test.go:126: id:c->0xc000006630:&{ID:c A:5 B:6}
req_test.go:126: id:d->0xc000006630:&{ID:d A:7 B:8}
req_test.go:126: id:a->0xc000006630:&{ID:a A:1 B:2}
--- PASS: TestUnexpectedFun (0.00s)
PASS
可以看到,结果跟方法1是一样的。
以上两种处理方案,看个人喜好选择采用。个人更倾向于第一种方案,从踩坑来看,如果时间长了忘记了踩过的这个坑,写了同样的逻辑,那么新代码就不会产生结果与预期不符的问题。
个人建议:如果你有一个好的习惯,那一定不要为了迎合,而放弃了你这个好习惯。
就像这个bug的产生一样,往常我的一贯写法都是第一种方案,但是因为某个同学说第一种写法产生的源数据,容易在后期被修改,所以我把写法改成了第二种,但是这种临时变量的修改是可控的,需要修改时才会被修改。而如果我保持了原来的代码风格,那这个坑就可以避免。
|