直接用for range + go func()处理切片分块易引发闭包变量覆盖、竞态、漏计数、错误丢失等问题;应改用channel解耦任务分发与执行,固定worker数,传参而非捕获变量,统一通过resultChan收集结果。

直接用 go 启动一堆 goroutine 处理切片分块,是最常见也最容易出错的做法。它能跑通,但不等于安全、可控、可维护。
为什么不能直接 for range + go func() {}
看似简单:把数据切片、每个 goroutine 处理一块、sync.WaitGroup 等待——但实际踩坑点密集:
-
wg.Add(1)写在循环里却没加锁或提前预设总数,容易漏计数或 panic - 闭包捕获循环变量
i或lines[i:end],导致所有 goroutine 实际处理同一段内存(经典“变量被覆盖”问题) - 结果写入共享 slice 时没加锁或没通过 channel 传递,引发竞态(
go run -race一跑就报) - 没有错误收集机制,某个 goroutine panic 或返回 error,主流程完全感知不到
用 channel + 固定 worker 数量更稳
把“任务分发”和“执行”解耦,用带缓冲 channel 做队列,固定数量 worker 消费,天然规避闭包陷阱和共享写冲突:
关键点:
立即学习“go语言免费学习笔记(深入)”;
- 任务通道声明为
chan Task(只读/只写类型明确),缓冲大小按压测结果设(比如make(chan Task, runtime.NumCPU()*4)) - worker 启动时传入指针
*sync.WaitGroup,并在defer wg.Done()前确保wg.Add(1)已调用 - 每个 task 结构体里打包输入数据、上下文、输出目标(如
resultChan),避免闭包捕获外部变量 - 错误统一走
resultChan <- Result{Err: err},主 goroutine 收集后判断是否需中止
需要超时或强制刷批?别轮询 len(ch) == cap(ch)
想“满 100 条就处理”或“5 秒没凑够也发出去”,千万别用 select { case ch <- x: default: process() } 这种方式——process() 执行时其他 goroutine 仍可能往通道写,造成数据错乱或 panic。
正确姿势是换掉缓冲通道,改用内存聚合:
- 起一个单独的聚合 goroutine,接收原始数据流
- 用
[]Data存缓存,配合time.Timer和select等待len(batch) == limit或定时器触发 - 聚合完成后再整体投递给下游 processor(可再开 goroutine 并行处理)
- 这样既无竞态,又可控,还省去通道容量管理的脑力消耗
批量结果怎么汇总才不出错
别用全局 map 或 slice 加互斥锁——代码散、易漏锁、性能差。两个轻量方案:
- 用
sync.Map:适合 key 明确(如 ID)、写多读少场景,Store(key, val)并发安全,最后Range遍历即可 - 用结果 channel:每个 worker 写
resultChan <- struct{ID int; Count int; Err error},主 goroutine 用for i := 0; i 顺序收,天然有序且无锁
真正难的不是启动多少 goroutine,而是让它们不互相踩脚、不丢错误、不卡死、不泄漏。控制权要收回来——靠 channel 队列、靠固定 worker、靠单点聚合、靠明确的结果通道,而不是靠“尽量少写锁”的侥幸心理。


















