可控后台任务队列需用带缓冲channel配合固定数量goroutine:生产者写入chan *Task,N个goroutine用for range消费,缓冲容量控制积压上限,goroutine数控制并发度。

goroutine本身不是队列,得靠channel + for-range协调
直接用 go func() {}() 启一堆 goroutine 处理任务,不加控制就是裸奔——任务积压、panic 泛滥、内存暴涨。真正可控的后台任务队列,核心是用 chan 做缓冲,再配固定数量的 goroutine 消费它。
典型结构:一个写入端(生产者)往 chan *Task 里塞任务,N 个 goroutine 从该 channel 里读取并执行。channel 容量决定积压上限,goroutine 数量决定并发度。
- 别用无缓冲 channel(
make(chan *Task)),否则生产者一发任务就阻塞,失去“队列”意义 - 缓冲区大小建议设为合理上限,比如
make(chan *Task, 1000),避免 OOM 又不至于完全丢任务 - 消费 goroutine 要用
for task := range ch,而不是for { ,否则 channel 关闭后会 panic
如何安全关闭任务队列并等待所有任务完成
关闭队列不是简单 close(ch) 就完事。必须确保:① 不再写入;② 所有已入队任务被执行完毕;③ 消费 goroutine 正常退出。常见错误是 close 后立刻 return,导致部分任务被丢弃。
推荐用 sync.WaitGroup 配合 channel 关闭信号:
立即学习“go语言免费学习笔记(深入)”;
var wg sync.WaitGroup
ch := make(chan *Task, 1000)
<p>// 启动 3 个 worker
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for task := range ch {
task.Do()
}
}()
}</p><p>// 提交任务...
ch <- &Task{...}
ch <- &Task{...}</p><p>// 关闭写入端
close(ch)</p><p>// 等待所有 worker 执行完
wg.Wait()
- 必须在
close(ch)前停止所有对ch的写操作,否则会 panic: send on closed channel -
WaitGroup的Add必须在go之前调用,不能放在 goroutine 内部 - 如果任务本身可能 panic,需在 worker 内加
defer recover(),否则整个 goroutine 退出,WaitGroup计数不减
为什么不要用 time.Sleep 做任务延迟调度
想让某个任务 5 秒后执行?别在 goroutine 里写 time.Sleep(5 * time.Second); task.Do()。这种写法会占用一个 goroutine 整整 5 秒,严重浪费调度资源,尤其当延迟任务成百上千时,goroutine 数爆炸。
- 真正可扩展的做法是用
time.AfterFunc或timer.Reset+ 单独的定时器 goroutine - 或者引入轻量级延迟队列,如用最小堆维护下次执行时间,配合一个 goroutine 拿出到期任务推给主工作 channel
- 第三方库如
github.com/robfig/cron或github.com/hibiken/asynq更适合复杂调度场景,原生 goroutine 不负责时间管理
worker panic 导致队列静默失效,怎么捕获
一个 worker 因未处理的 panic 退出后,for range 循环终止,该 goroutine 彻底消失,但其他 worker 还在跑——你根本察觉不到少了一个消费者,直到积压越来越多、超时越来越多。
最简单的防护是在每个 worker 最外层加 recover:
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("worker panic: %v", r)
}
}()
for task := range ch {
task.Do()
}
}()
- 仅 recover 不够,要记录日志甚至上报监控,否则等于掩耳盗铃
- 如果任务执行逻辑本身可能 panic(比如解析用户传来的 JSON),应在
task.Do()内部也做 recover,而不是全丢给 worker 层 - 更健壮的做法是让 worker 在 recover 后自动重启,但需加重启次数限制,防住死循环 panic
实际跑起来之后,最容易被忽略的是 channel 缓冲区和 worker 数量的组合效果——它决定了你的系统是“稳态吞吐高”,还是“偶发积压就雪崩”。这两个参数得结合真实任务耗时和到达频率压测调整,没法靠理论值拍板。


















