【Go实战】如何正确使用切片数据

这篇文章是为了给自己烙印上正确使用切片的印记,并加深对切片地址的理解。原因是线上代码产生的结果不符合预期,排查下来,是因为对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的产生一样,往常我的一贯写法都是第一种方案,但是因为某个同学说第一种写法产生的源数据,容易在后期被修改,所以我把写法改成了第二种,但是这种临时变量的修改是可控的,需要修改时才会被修改。而如果我保持了原来的代码风格,那这个坑就可以避免。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值