
在 go 的循环中启动 goroutine 时,若直接传递切片等引用类型变量,可能因底层底层数组被后续迭代覆盖而导致 goroutine 内部读取到错误值——本质是共享底层数组引发的数据竞争。
在 go 的循环中启动 goroutine 时,若直接传递切片等引用类型变量,可能因底层底层数组被后续迭代覆盖而导致 goroutine 内部读取到错误值——本质是共享底层数组引发的数据竞争。
这个问题的核心在于:Go 切片是引用类型,包含指向底层数组的指针、长度和容量。即使你将切片 c 作为参数传入 goroutine,也只是复制了这个“头”(header),并未复制其背后的数据数组。
在你的代码中,Combinations 函数内部复用同一个底层数组 s(通过 make([]int, m) 创建),每次调用 emit(s) 时实际传递的是该数组当前状态的切片视图。但 s 在整个递归生成过程结束后,其所有元素均被最后一次写入覆盖为 5(即 n-1 = 6-1)。而 goroutine 是异步延迟执行的——它们往往在 Combinations 完全返回后才真正开始运行,此时看到的 c 虽然长度/起始位置正确,但底层数据早已被污染。
✅ 正确做法是:在启动 goroutine 前,显式创建一个独立的副本(deep copy),确保每个 goroutine 拥有自己专属的数据副本:
使用 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) // 传入副本,而非原始切片
})
⚠️ 注意事项:
-
copy(dst, src)要求dst已分配足够空间,且len(dst) >= len(src); - 对于嵌套结构(如
[][]int或含指针的 struct),需递归拷贝;本例中仅一维[]int,make + copy即可满足; - 若性能敏感且组合数量极大,可考虑预分配副本池(sync.Pool)避免频繁内存分配;
- 使用
-race检测器虽能发现部分数据竞争,但此问题不属于传统竞态(无并发读写同一内存地址),而是逻辑时序错误,故-race不会报错——务必理解原理,不能依赖工具兜底。
总结:在循环中启动 goroutine 并捕获切片、map 或 struct 中含指针字段的变量时,永远假设「原始数据可能被后续迭代覆写」。安全第一原则是——需要时就深拷贝,不省略、不侥幸。










