直接用 goroutine 会崩是因为无节制并发导致内存暴涨、调度过载、连接打爆;必须用 Worker Pool 控制并发,核心是任务队列、固定 worker 数、结果收集,并通过 context 精确管理超时与取消。

为什么直接用 goroutine 会崩,而不是加个 channel 就完事
Go 里随手 go f() 看似简单,但任务量一上来,内存暴涨、调度器过载、DB 连接被打爆都是常态。Worker Pool 不是“锦上添花”,而是防止 runtime: out of memory 或 too many open files 的刚需控制手段。
核心逻辑就三块:任务队列(chan Job)、固定数量的 worker goroutine、结果收集(可选 chan Result)。关键不在“启多少 goroutine”,而在“怎么堵住无节制并发”。
- 别把任务数据全塞进 channel —— 大结构体传值开销大,优先传指针或 ID,让 worker 自行加载
-
buffered chan容量别设成math.MaxInt或盲目等于 worker 数,100~1000 通常是安全起点 - worker 必须有明确退出路径,不能靠 channel 关闭“猜”是否该停;用
context.Context控制生命周期更可靠
如何写一个带超时和取消的 Worker Pool
生产环境的任务不可能无限等。不加 context.WithTimeout 或 context.WithCancel,worker 卡死、上游已放弃、下游还在傻等,是常见雪崩源头。
每个 worker 应该在自己的 goroutine 内监听 ctx.Done(),而不是只在接收任务时检查一次。
立即学习“go语言免费学习笔记(深入)”;
- 任务入队前就绑定
context.WithTimeout(parentCtx, 5*time.Second),不是 worker 启动后才加 - worker 执行具体业务时,要把
ctx透传给所有可能阻塞的调用,比如http.Client.Do、db.QueryContext - 关闭 pool 时,先关任务 channel,再
cancel(),最后waitGroup.Wait()—— 顺序错会导致 panic 或 goroutine 泄漏
for {
select {
case job, ok := <-pool.jobs:
if !ok {
return
}
pool.handleJob(ctx, job) // ctx 是带 cancel 的那个
case <-ctx.Done():
return
}
}
channel 关闭后还读,为什么有时 panic 有时静默
对已关闭的 chan Job 执行 <-ch 不 panic,但会立即返回零值;而 job, ok := <-ch 中的 ok 为 false 才是正确判断依据。很多人只看值,忽略 ok,结果把零值当有效任务执行,引发空指针或逻辑错。
- 永远用
job, ok := 形式读取,<code>ok == false表示 channel 已关,worker 应立即退出 - 不要在多个 goroutine 里重复
close(pool.jobs),会导致panic: close of closed channel - 关闭 channel 的职责应唯一:通常是启动 pool 的主 goroutine,在发完所有任务后关闭
为什么用 sync.WaitGroup 而不用 channel 来等 worker 结束
用 doneChan := make(chan struct{}, workerCount) 让每个 worker 结束时发一个信号,听起来合理,但容易掉坑:如果某个 worker panic 了没发信号,主 goroutine 就永久阻塞。而 sync.WaitGroup 的 Done() 调用是幂等且轻量的,配合 defer wg.Done() 更稳妥。
-
wg.Add(1)必须在go worker()之前调用,否则存在竞态 - worker 函数入口第一行就
defer wg.Done(),确保无论正常返回还是 panic 都能计数减一 - 别在 worker 里调用
wg.Add(),WaitGroup 不支持运行时动态增减
WithWorkerCount(n int) 构造函数。










