安全拆分大切片需按索引区间切分,每个goroutine只读data[start:end],用channel收集结果;传整个切片会导致共享底层数组引发竞态。

Go语言并发任务拆分不是“把循环套个 go 就完事”,而是明确划分数据边界、隔离执行单元、避免共享竞争——否则你启动100个 goroutine,实际都在抢同一片切片,结果比串行还慢,还触发竞态(go run -race 一跑就报)。
怎么安全地把一个大切片拆给多个 goroutine 并行处理?
核心是“按索引区间切分”,而非复制数据或用全局变量。每个 goroutine 只读自己分到的那一段,不碰别人的内存。
- 先算总长度和期望并发数(比如
numWorkers := runtime.NumCPU()) - 用整除+取余确定每段长度:主段长
chunkSize := len(data) / numWorkers,余数部分匀给前几个 worker - 每个 worker 接收
start, end int参数,在闭包里只操作data[start:end] - 绝对不要让多个 goroutine 同时写同一个
[]int或 map —— 要写结果,用 channel 收集,或用sync.Mutex保护写入点(但更推荐 channel)
func parallelSum(data []int) int {
numWorkers := runtime.NumCPU()
chunkSize := len(data) / numWorkers
var wg sync.WaitGroup
ch := make(chan int, numWorkers)
for i := 0; i < numWorkers; i++ {
start := i * chunkSize
end := start + chunkSize
if i == numWorkers-1 {
end = len(data) // 最后一段吃掉余数
}
wg.Add(1)
go func(s, e int) {
defer wg.Done()
sum := 0
for _, v := range data[s:e] {
sum += v
}
ch <- sum
}(start, end)
}
wg.Wait()
close(ch)
total := 0
for partial := range ch {
total += partial
}
return total}
为什么直接 go f(slice) 会出问题?
常见错误是把整个切片传进每个 goroutine,以为“各自处理”——但切片头(len、cap、ptr)是值传递,底层数组指针仍是共享的。如果函数里有写操作(比如 slice[i] = x),就会产生竞态;即使只读,也失去了“数据局部性”,缓存效率低,还可能因 GC 延迟回收整块内存。
立即学习“go语言免费学习笔记(深入)”;
- 现象:运行时加
-race报Data Race on slice element - 本质:多个 goroutine 同时访问同一数组地址,哪怕只是读,也可能被编译器优化成非原子访存(尤其在某些架构上)
- 修复方式:要么传子切片(
data[i:j]),要么传副本(copy(dst, src),但注意成本),要么用只读接口抽象
什么时候该用 Worker Pool 而不是“一任务一 goroutine”?
当你面对的是**不确定数量的任务流**(如 HTTP 请求、消息队列消费),而不是一次性静态数据时,“为每个请求开 goroutine”极易失控。这时必须用 Worker Pool 控制并发上限。
- 典型信号量实现:
sem := make(chan struct{}, 10),每个 worker 启动前先sem ,退出时再 - 搭配
context.Context实现超时/取消:worker 内部监听ctx.Done(),收到信号立即返回,不等任务做完 - 别用
time.Sleep等待 worker 结束——它不可靠、不精确、无法响应中断;改用sync.WaitGroup+channel收集结果
最常被忽略的一点:任务拆分后,若某段数据处理耗时远超其他段(比如某段含大量空值需跳过),整个并行流程会被拖慢。这时候需要动态负载均衡(如使用带缓冲的 task channel 分发),而不是静态切分——但那是另一个层级的问题了。










