goroutine + channel 适合单机、轻量、可丢失任务,如发通知、写日志、刷新本地缓存;因其不持久化,进程崩溃则 pending 任务丢失;常见错误包括使用无缓冲 channel 导致主流程卡死,或未关闭 channel 致 worker 永久阻塞,须用带缓冲 channel(如 make(chan task, 100))并固定 worker 数量。

goroutine + channel 适合什么场景
只适合单机、轻量、可丢失的任务,比如发通知、写日志、刷新本地缓存。它不持久化,进程一挂,所有 pending 任务就没了。
常见错误现象:taskCh := make(chan Task) 写成无缓冲 channel,高并发提交时主流程直接卡死;或者忘了 close(taskCh),导致 worker 协程永远阻塞在 range 上。
- 必须用带缓冲的 channel,例如
make(chan Task, 100) - worker 数量要固定,用
for i := 0; i 启动,别动态 new goroutine - 每个 worker 内部加
defer func() { recover() }(),防止单个 panic 杀掉整个协程池 - 任务函数签名建议是
func(context.Context) error,方便后续加超时和取消
asynq 为什么比自研更稳
因为 Redis 天然支持持久化、重试队列、延迟投递和手动重入——这些不是“锦上添花”,而是生产环境里避免丢任务的底线能力。
典型问题:你用 time.AfterFunc 模拟延时任务,但进程重启后,所有未触发的定时器全丢;而 asynq 的 asynq.EnqueueIn 是把任务写进 Redis,服务重启后照样按时执行。
- 失败任务自动进
asynq:retry队列,可配MaxRetry: 3和指数退避 - 任务 ID 是唯一且可追溯的,日志里打
task.ID就能串起完整链路 - HTTP API(默认
/asynq/)能查状态、重试、删失败任务,不用翻日志 - 别把参数塞闭包里,例如
func() { sendMail(to) }—— 序列化失败,重试时to变空指针
什么时候必须上消息队列
当任务跨服务、要保障至少一次交付、或需解耦发布方与消费者时,RabbitMQ 或 Kafka 不是“高级选项”,而是必要基础设施。
比如订单创建后触发库存扣减、积分更新、短信发送三个动作:如果用本地 channel,一个服务崩了,另外两个也跟着不可控;而用消息队列,每个消费者独立 ACK,失败只影响自己那条链路。
-
RabbitMQ适合需要灵活路由(如 topic/exchange)、死信队列做兜底重试的场景 -
Kafka更适合事件溯源、高吞吐日志类任务,但要注意 consumer group offset 管理 - 千万别用
channel做跨进程通信——它连本机两个 goroutine 间传数据都靠内存共享,跨服务根本不存在“队列”这回事 - 消息体必须结构化(如 JSON),字段带
id、timestamp、source_service,方便幂等和追踪
任务结果怎么拿回来
异步任务天然不返回值,所谓“拿结果”本质是换一种交互模式:要么轮询状态,要么回调通知,要么让下游主动上报。
直接从 go task() 里想 return result 是错的——goroutine 没有返回通道,除非你显式加 resultCh chan Result 字段并管理生命周期。
- 简单方案:任务完成时往 Redis 写个
task:{id}:result,调用方用GET轮询,加EXPIRE防止堆积 - 推荐方案:任务成功后发一条
task.completed事件,主服务监听该 topic 更新状态 - 别在 handler 里同步 HTTP 回调上游——网络超时会拖垮整个 worker,应另起 goroutine 并设 timeout
- 如果真要同步等结果,说明这不是异步任务,该走 gRPC 或 REST 直接调用
chan、asynq 还是 RabbitMQ。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











