直接用 chan 做任务队列易 panic,因向已关闭 channel 发送数据会触发“send on closed channel”;无缓冲 channel 在多 goroutine 读写时易死锁;必须封装控制层,如带缓冲 channel、sync.waitgroup 配合 close、select+default 防阻塞、context 控制生命周期。

为什么直接用 chan 做任务队列容易 panic
Go 的 chan 本身不是线程安全的任务队列,往已关闭的 channel 发送数据会立即 panic: send on closed channel;而多个 goroutine 同时读/写无缓冲 channel,还可能因阻塞导致死锁或资源堆积。实际业务中,任务提交和执行节奏不一致,必须加控制层——不是“能不能用 channel”,而是“怎么封装才不崩”。
- 别裸用
make(chan Task),至少包装成结构体,内嵌sync.Mutex或用带缓冲的 channel 控制吞吐 - 关闭 channel 前必须确保所有 sender 已退出,推荐用
sync.WaitGroup+close()配合,而不是靠超时或标志位 - 如果任务需要返回结果,别用
chan<interface></interface>混传,定义明确的响应结构体,例如type Result struct { ID string; Err error; Data []byte }
用 select + default 防止任务提交阻塞
用户请求触发任务时,不能让 HTTP handler 卡在 taskChan 上。尤其当 worker 数量少、channel 缓冲满时,<code>select 是唯一可控的非阻塞写法。
select {
case taskChan
- 缓冲大小不是越大越好:
make(chan Task, 100)可能掩盖 worker 处理慢的问题,建议设为 worker 数 × 2~3 - 不要在
default分支里起 goroutine 异步重试,这会绕过背压控制,可能雪崩 - 若需排队等待,改用带超时的
select:case taskChan 和 <code>case
Worker 启动与 graceful shutdown 的关键点
启动 worker 时用 for range 读 channel 是常见写法,但 range 在 channel 关闭后自动退出,无法区分“正常关机”和“意外关闭”。真正可控的 shutdown 必须依赖 context.Context。
func startWorker(ctx context.Context, ch
- 启动时用
go startWorker(ctx, taskChan),shutdown 时调用cancel(),再close(taskChan) - 不要在 worker 内部捕获
panic后继续循环——未处理的 panic 会丢失,应让上层 recover 并记录日志 - worker 数量建议固定(如
runtime.NumCPU()),避免动态伸缩引入竞争条件
要不要用第三方库?machinery 和 asynq 的真实差异
如果你只需要内存级、单机、无持久化、低延迟的任务调度,手写 channel 方案更轻、更可控;一旦涉及失败重试、延迟任务、Web 控制台、Redis 存储,asynq 是目前最省心的选择——它底层仍用 channel 调度,但把序列化、心跳、锁、状态同步这些坑全填了。
-
machinery依赖 AMQP(如 RabbitMQ),配置重、学习成本高,适合已有消息中间件的团队 -
asynq默认用 Redis,支持asynq.RedisClientOpt{Addr: "localhost:6379"}一行接入,且提供asynq.ServeMux直接暴露 metrics 和 UI - 手写方案最难补的是“任务幂等性”和“崩溃恢复”——进程挂了,正在跑的任务怎么办?
asynq把任务状态存在 Redis,重启后自动续跑
channel 是骨架,但生产环境的任务队列,骨架之外全是血肉。没监控、没重试、没持久化的 channel 队列,上线三天就会变成故障根因。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











