结论:关键路径、需重试、要监控的任务必须走消息队列;轻量、单机、可丢的才考虑goroutine+channel。核心判断标准是任务失败是否允许丢失、是否跨服务协作、是否要求最终一致性。

直接说结论:别一上来就用 goroutine 跑任务,先判断任务性质——关键路径、需重试、要监控的,必须走消息队列;轻量、单机、可丢的,才考虑本地 goroutine + channel 控制池。
很多团队踩坑都是因为没分清“异步”和“可靠异步”的区别:一个 go sendEmail() 看似解耦,但进程挂了任务就丢了,日志里连 trace_id 都没留,根本没法查。
什么时候该用 RabbitMQ/Kafka 而不是 goroutine?
核心判断标准是:任务失败是否允许丢失、是否需要跨服务协作、是否要求最终一致性。
- 任务涉及资金、订单状态变更、用户通知等关键业务 → 必须用消息队列,比如
RabbitMQ(需 ACK + DLX)或kafka-go(高吞吐+分区有序) - 多个微服务都要响应同一事件(如“用户注册成功”触发积分、风控、推荐)→ 消息队列天然支持 fanout / topic 路由
- 需要延迟执行(如 30 分钟后发召回短信)→
RabbitMQ的 TTL + 死信队列,或asynq的Delay参数 - 本地日志写入、缓存刷新这类非关键操作 → 可用 goroutine,但必须加
recover()和超时控制
goroutine 池怎么写才不炸内存?
常见错误是 for range events { go handle(e) } —— 并发数失控,OOM 报警半夜响。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用带缓冲的
chan Task做任务队列,容量设为合理上限(如make(chan Task, 100)) - worker 数量固定,不要动态扩缩:5–20 个足够,取决于任务 I/O 密集度
- 每个 worker 必须包裹
defer recover(),否则 panic 会让整个 goroutine 死掉,队列卡住 - 单任务必须带
context.WithTimeout(ctx, 30*time.Second),避免某个任务 hang 住整个 worker - 提交任务时检查 channel 是否已满,满则拒绝或降级(比如写本地
outbox表再异步重试)
asynq 或 machinery 选哪个?
别被文档吓住,选型只看两点:你用什么存储、要不要 Web UI。
- 用 Redis 且需要实时查看任务状态、重试、延迟调度 → 直接上
asynq,自带asynqmonWeb 界面,集成prometheus指标开箱即用 - 已有 Kafka/RabbitMQ 基础设施,想复用 broker 做任务分发 → 选
machinery,支持多种broker,但需自己搭监控和重试策略 - 两者都要求任务函数必须幂等:
asynq默认最多重试 3 次,machinery需手动配置RetryCount和RetryInterval - 注意:
asynq的task.ID是字符串,不能重复;machinery的UUID由库生成,但自定义 ID 需额外处理
异步任务链路里最容易漏掉的三件事
不是代码写完就完事,漏掉任何一项,线上出问题时你就只能靠猜。
-
trace_id必须透传:从 HTTP header 注入,序列化进消息体,消费者反序列化后继续打日志,否则链路断在队列里 - 消费者必须手动
Ack(RabbitMQ)或CommitOffsets(Kafka),autoAck = true 是生产环境大忌 - 所有任务状态(pending/processing/success/failed)要落库或 Redis,提供
/task/{id}查询接口,别指望靠日志翻找
真正难的从来不是“怎么启动 goroutine”,而是“怎么让异步变得可查、可控、可恢复”。任务进了队列不等于完成了,它只是刚出发。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










