直接用 for range tasks { go handle(task) } 会导致内存暴涨、调度卡顿甚至 oom;应采用固定 worker 数量 + 任务 channel + sync.waitgroup 的可控并发模式。

直接用 for range tasks { go handle(task) } 启动协程,任务量一上来就会崩——不是代码逻辑错,而是内存和调度器先扛不住。唯一靠谱的方案是固定 worker 数量 + 任务 channel + sync.WaitGroup 协同生命周期。
为什么 go handle(task) 不能乱用
每个 goroutine 默认栈至少 2KB,10 万任务就吃掉 200MB+ 内存;调度器要维护所有 goroutine 状态,数量失控后延迟毛刺明显,GC 压力陡增;更隐蔽的是下游被打穿:比如 DB 连接池设了 50,你却并发发 1000 条 SQL,结果全是 dial tcp: lookup xxx: no such host 或 connection refused,根本查不到 Go 层面的问题。
-
go f()是放养式并发,控制权完全交给输入规模 - 没超时、没取消、没等待机制,失败任务难追踪
- 闭包捕获循环变量(如
for i := range tasks { go func() { process(tasks[i]) }() })会导致所有协程处理最后一个 task
如何用 chan Task 和 sync.WaitGroup 搭最简 worker pool
核心就三件事:建带缓冲或无缓冲的任务通道、起固定数量长期存活的 worker、用 WaitGroup 等它们全退干净。不要用大缓冲 channel 当“保险”,它只会掩盖处理瓶颈,让上游卡死在 tasks 上。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 任务 channel 类型选
chan Task还是chan *Task,取决于Task大小:小结构体(2–3 字段)传值更安全;大对象或需修改字段,传指针但必须防数据竞争 - worker 数量建议设为 CPU 核心数的 1.5–2 倍;IO 密集型(如 HTTP 请求)可设到 16–32;CPU 密集型别超核数
-
WaitGroup.Add(n)必须在启动 worker 前调用,且数值固定;每个 worker 结束前必须wg.Done(),推荐用defer wg.Done() - worker 读 channel 要用
for task, ok := range jobs { if !ok { break } ... },不能只写for range jobs——否则关 channel 后没法做清理(比如写日志、归还资源)
如何安全关闭并等所有任务跑完
关闭不是「立刻停」,而是「不再收新任务 + 等旧任务自然结束」。close(jobs) 只影响接收端,且只能由生产者调用一次;它不会中断正在运行的 goroutine,也不会让已从 channel 取出但尚未执行完的 task 提前退出。
- 正确顺序:所有任务发完 →
close(jobs)→wg.Wait() - 千万别在 worker 里
close(jobs),会 panic:close of closed channel - 如果任务本身阻塞(如 HTTP 请求没设 timeout),
wg.Wait()会永远卡住;必须给 task 的执行逻辑传入带 timeout 的context.Context - 不要依赖
range jobs自动退出来当 shutdown 信号——它不保证你能在退出前做 cleanup
真正容易被忽略的点是:worker 里没处理好 panic,或者忘了 defer wg.Done(),就会导致 wg.Wait() 死等;还有就是任务 channel 缓冲区设太大,让上游误以为“发得进去”就继续猛塞,实际下游早积压成山了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










