worker pool 是 go 并发落地的底线配置,因 goroutine 非零成本、下游服务限流及闭包陷阱等风险,直接 for range 启动协程极易导致内存溢出、429 错误或数据竞争。

直接用 for range tasks { go handle(task) } 启动协程处理批量任务,99% 的情况会出问题——不是逻辑错,是资源崩了才暴露。Worker Pool 不是“高级技巧”,而是 Go 并发落地的底线配置。
为什么 go handle(task) 不能乱用
goroutine 轻量 ≠ 零成本。每个默认栈 2KB,10 万任务就是 200MB+ 内存;调度器要维护所有 goroutine 状态,数量失控后延迟毛刺明显;下游服务(比如数据库连接池、HTTP client)通常有硬限制,瞬间打满直接返回 429 Too Many Requests 或 dial tcp: lookup failed。
- 常见错误现象:
runtime: out of memory、panic: send on closed channel、HTTP 请求大面积超时 - IO 密集型任务(如 HTTP 抓取、日志写入)建议 worker 数设为 10–100;CPU 密集型才考虑接近
runtime.NumCPU() - 别按“机器核数”拍脑袋定 worker 数,要看你调的是什么服务、它能扛多少并发连接
jobs := make(chan Job, N) 的缓冲区怎么选
带缓冲 channel 是生产环境首选,无缓冲只适合强背压且 worker 绝对可靠的场景(比如内部实时流控)。缓冲区大小不是越大越好,也不是越小越安全。
-
N一般设为预期最大待积压任务数,比如每秒进 100 个任务、worker 每秒处理 20 个,那缓冲区至少要 500 才不至于频繁阻塞 - 设太大(如
make(chan Job, 10000))会掩盖积压问题,任务在队列里躺太久才被发现 - 设太小(如
make(chan Job, 1))会让上游提交方频繁阻塞,尤其在 HTTP handler 这类短生命周期上下文中,容易拖垮整个请求链路
闭包陷阱和 job 变量捕获必须当场解决
下面这段代码是典型错误:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
for _, j := range tasks {
go func() {
j.Process() // 所有 goroutine 共享同一个 j,实际只处理最后一个 task
}()
}
根本原因:循环变量 j 在每次迭代中复用地址,goroutine 实际捕获的是变量引用,不是值。
- 正确写法:传参进 goroutine,
go func(job Job) { job.Process() }(j) - 或在循环内定义新变量:
job := j; go func() { job.Process() }() - 如果
Job包含slice或map字段,务必确认只读或做深拷贝,否则触发data race
关闭流程不按顺序就会死锁或 panic
Worker Pool 关闭不是调一个函数的事,是三步协同:停投递 → 等消费 → 收结果。漏一步,轻则丢数据,重则 panic: send on closed channel。
- 必须由生产者(主 goroutine)调用
close(jobs),worker 里关会 panic - worker 必须用
for job := range jobs或job, ok := 判断通道是否关闭,不能只用 <code>select监听却不判ok -
sync.WaitGroup.Add()要在启动 worker 前完成,Done()必须在每个 worker 退出前执行(推荐defer wg.Done()) - 结果 channel 如果带缓冲,发送前要用
select { case resultChan 防写满 panic
最常被忽略的点:worker 退出前必须确保所有资源已释放(比如关闭 HTTP body、归还 DB 连接),range 自动退出不会等你写完日志再走。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










