
本文详解如何正确使用通道在多个 goroutine 之间传递切片数据,彻底规避因共享变量(如全局或闭包捕获的切片)引发的数据竞争问题,并提供可验证的无竞态、确定性输出的并发代码范式。
本文详解如何正确使用通道在多个 goroutine 之间传递切片数据,彻底规避因共享变量(如全局或闭包捕获的切片)引发的数据竞争问题,并提供可验证的无竞态、确定性输出的并发代码范式。
在 Go 并发编程中,一个常见误区是试图让多个 goroutine 共享并修改同一个切片变量(例如示例中的全局变量 x),再通过通道传递其地址或引用——这不仅违背 Go “不要通过共享内存来通信,而应通过通信来共享内存”的设计哲学,更会直接触发数据竞争(data race)。运行 go run -race main.go 时报告的 4 处竞态,根源正在于此:多个 goroutine 同时读写 x,且 main 函数在 wg.Wait() 完成前就可能访问未同步的 slice。
✅ 正确做法:每个 goroutine 独立构造切片,通道只传递值
关键原则是:通道应传递不可变或独立拥有的数据副本,而非共享可变状态。[]string 是引用类型,但只要每个 goroutine 自行调用 cat_strings 创建新切片(即返回全新底层数组),该切片就是 goroutine 局部的,无需额外同步。
func main() {
var slice []string
var wg sync.WaitGroup
queue := make(chan []string, 10) // 缓冲通道,避免 goroutine 阻塞
wg.Add(10)
for i := 0; i < 10; i++ {
go func(i int) {
defer wg.Done()
// ✅ 每个 goroutine 独立生成新切片,不复用 x
result := cat_strings(i, "var")
queue <- result // 传递副本,安全
}(i)
}
// ✅ 启动协程等待所有任务完成并关闭通道
go func() {
wg.Wait()
close(queue) // 确保 range 能正常退出
}()
// ✅ 主 goroutine 同步消费通道,避免与 print 竞态
for t := range queue {
slice = append(slice, t...) // t 是独立切片,append 安全
}
fmt.Println(slice) // 所有数据已收集完毕,安全打印
}⚠️ 关键修复点说明
- 移除共享变量 x:原代码中 x 被所有 goroutine 共用,导致写入覆盖和竞态。新版中每个 goroutine 调用 cat_strings 返回新切片,完全隔离。
- 通道关闭时机精准:由专用 goroutine 在 wg.Wait() 后调用 close(queue),确保所有发送完成后再关闭,使 for range 可靠终止。
- 消费逻辑置于主 goroutine:不再另起 goroutine 异步收集结果,避免 slice 与 fmt.Println 的读写竞态;range 结束即代表全部数据就绪。
- 缓冲通道合理设置:make(chan []string, 10) 提供足够缓冲,防止 sender 因 receiver 未启动而阻塞(虽本例中 receiver 启动稍晚,但缓冲可兜底)。
? 验证与最佳实践
- 始终用 go run -race 检测竞态;
- 避免在 goroutine 间传递指向同一底层数组的切片(除非明确加锁或使用 sync.Pool);
- 若需聚合大量数据,考虑预分配 slice 容量(如 make([]string, 0, 20))提升 append 性能;
- 对于更复杂场景(如需按序输出),可结合 sync.WaitGroup + []chan 或使用 errgroup 等高级库。
遵循以上模式,你的并发程序将输出稳定、可预测的结果,彻底告别“运行 10 次得到 10 种答案”的非确定性问题。


















