worker pool 关闭时 goroutine 泄露的典型表现是程序退出后 pprof 显示大量 goroutine 停留在 runtime.gopark 或 chan receive 状态,http.Server.Shutdown 卡住或 SIGTERM 后进程不退出;根本原因是关闭逻辑与 worker 生命周期未对齐,需用 context 控制整个 worker 生命周期,配合带缓冲 channel 和 select + ctx.Done() 实现安全关闭。

worker pool 关闭时 goroutine 泄露的典型表现
程序退出后 pprof 显示仍有大量 goroutine 处于 runtime.gopark 或 chan receive 状态,http.Server.Shutdown 卡住、超时,或 SIGTERM 后进程不退出——这基本是 worker 未响应关闭信号、还在等任务或阻塞在 channel 读写上。
根本原因不是“没调 Close”,而是关闭逻辑和 worker 生命周期没对齐:worker 在 for job := range jobs 中阻塞,但 jobs channel 没被关闭,或关闭了但部分 worker 已经从 channel 取出 job 正在处理,没做完成确认。
- 别用
close(jobs)后直接 return:worker 可能刚读到 job 就遇到 channel closed,任务丢了 - 别只靠
context.WithTimeout包裹单个 job:超时只杀当前 job,worker 仍会继续取下一个 - 必须区分「不再收新任务」和「等已有任务做完」两个阶段,且两者都要可取消
用 context 控制 worker 生命周期(非仅 job)
每个 worker 应该监听一个贯穿其整个生命周期的 ctx,而不是只给单个 job 用。这样关闭时能立刻中断阻塞在 jobs channel 上的读操作,也方便在 job 执行中响应取消。
关键点在于:把 jobs channel 改成带缓冲的,并配合 select + ctx.Done(),避免 worker 因 channel 无数据而永久挂起。
立即学习“go语言免费学习笔记(深入)”;
for {
select {
case job, ok := <-jobs:
if !ok {
return // jobs closed, no more tasks
}
job.Do(ctx) // job 内部也要检查 ctx.Err()
case <-ctx.Done():
return // shutdown signal received
}
}-
jobs缓冲区大小建议设为 worker 数量的 1–2 倍,防止提交任务时因无空闲 worker 而阻塞主流程 - worker 启动时传入
ctx,不要在循环内重新context.WithCancel,否则关不掉 - 如果 job 本身含 I/O(如 HTTP 请求、DB 查询),必须显式传入该
ctx,否则ctx.Done()对它无效
Shutdown 阶段的三步协作协议
优雅关闭不是单方面通知,而是 dispatcher 和 worker 之间的协作:先停收、再等跑完、最后强制清理。漏掉任一环都会导致卡死或丢任务。
-
Step 1(停收):调用
close(jobs),同时停止向jobs发送新任务 -
Step 2(等跑完):启动一个独立 goroutine,用
sync.WaitGroup记录正在执行的 job 数量;worker 每开始一个 job 就wg.Add(1),结束时wg.Done() -
Step 3(兜底):给
wg.Wait()加超时(比如 30s),超时后不再等,直接返回——此时残留的 job 应已在内部响应ctx.Err()主动退出
注意:WaitGroup 必须在所有 worker 启动前初始化,且 wg.Add(1) 要放在 job 实际执行前(比如在 job.Do() 调用之前),不能放在 for 循环开头,否则会多计数。
channel 关闭时机与 panic 风险
对 jobs channel 调用 close() 是安全的,但对结果 channel(如 results)必须格外小心:多个 worker 同时往同一个 results channel 写,一旦提前 close,就会触发 panic: send on closed channel。
正确做法是让 dispatcher(而非 worker)负责关闭 results,并在所有 worker 退出后再 close:
- worker 只管
select { case results ,不碰 close - dispatcher 在
wg.Wait()返回后,再close(results) - 如果使用
range results消费结果,必须确保 consumer 在 dispatcher close 后才退出,否则可能漏收最后几条
最容易被忽略的是:当 worker 因 ctx 取消提前退出时,它可能已经把部分中间状态写进了日志或 DB,但没来得及发 result。这种“半完成”状态需要业务层自己定义幂等或补偿逻辑,context 和 channel 机制管不了这个。










