
在 go 的循环中启动 goroutine 时,若直接传递引用类型(如切片)变量,可能因底层底层数组被后续迭代覆盖而导致所有 goroutine 读取到错误或一致的旧值——本质是共享底层数组引发的数据竞争。
在 go 的循环中启动 goroutine 时,若直接传递引用类型(如切片)变量,可能因底层底层数组被后续迭代覆盖而导致所有 goroutine 读取到错误或一致的旧值——本质是共享底层数组引发的数据竞争。
这个问题的核心在于 Go 切片的内存模型:切片本身是轻量级结构体(包含指针、长度、容量),其数据存储在底层数组中。在您的代码中,Combinations 函数内部复用同一个底层数组 s 来生成每组组合,并通过 emit(s) 将切片 s 传入回调函数。虽然每次调用 emit 时 c 看似是“新”切片,但它始终指向同一块底层数组内存。
关键时间线如下:
-
Combinations在递归结束后,将s的所有元素设为5(即n-1,因totalHashFields = 6); - 所有
go func(c []int) { ... }(c)的 goroutine 实际上是在Combinations完全退出后才开始执行; - 此时
s已被彻底覆盖,而c的底层数组正是s,因此所有 goroutine 打印出[5 5 5]或[5 5 5 5 5 5]——并非传参失败,而是数据已被覆盖。
✅ 正确做法:在启动 goroutine 前,对切片进行深拷贝(deep copy),即分配新底层数组并复制元素:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
Combinations(totalHashFields, i, func(c []int) {
fmt.Println("Outside goroutine:", c)
// 创建独立底层数组的副本
cpy := make([]int, len(c))
copy(cpy, c)
go func(c []int) {
fmt.Println("Inside goroutine:", c)
}(cpy)
})
⚠️ 注意事项:
-
c作为参数传递给 goroutine 是值传递(复制切片头),但不复制底层数组,因此必须显式make + copy; - 若组合结果需长期持有或并发修改,务必避免复用
s;也可考虑在Combinations内部每次emit前就做拷贝(更安全但略低效); -
-race检测不到此问题,因为它不是传统竞态(无同时读写),而是逻辑时序错误 + 共享可变状态,需开发者主动识别。
总结:在循环中向 goroutine 传递切片时,永远假设其底层数组可能被后续迭代重用。安全第一原则——只要 goroutine 生命周期超出当前迭代作用域,就必须深拷贝切片内容。










