Go异步任务不能仅用goroutine,必须持久化落盘;Redis Stream是轻量可靠首选,支持消费者组、ACK、重试与死信;gocraft/work和go-workers基于Redis List但无消费者组语义,可靠性弱于Stream。

Go 语言里做后台任务异步处理,不能只靠 go func() 一扔了事——进程挂了任务就丢,没重试、没状态追踪、没法横向扩展。真要落地,得选有持久化、可监控、带失败重试的方案,而不是用 channel + goroutine 自建“玩具队列”。
为什么 goroutine 直接起 worker 不适合生产环境
常见错误是 handler 里写 go sendEmail(args),看着简单,但问题一堆:
- 进程崩溃或机器重启,正在跑的 goroutine 全部消失,任务永久丢失
- 没入队确认机制,HTTP 请求超时或网络抖动时,任务可能根本没发出去
- 无法统计成功率、延迟、积压量,出问题只能翻日志盲猜
- 并发失控:每请求起一个 goroutine,QPS 上千时 goroutine 数爆炸,内存和调度开销反成瓶颈
真正需要的是「任务先落盘,再由独立 worker 进程拉取执行」,也就是解耦 + 持久化 + 可追溯。
Redis Stream 是最轻量又可靠的选型
不用额外部署 RabbitMQ 或 Kafka,Redis 6.2+ 自带 Stream,天然支持消费者组、ACK、重试、死信,Go 客户端(如 github.com/redis/go-redis/v9)封装成熟。
立即学习“go语言免费学习笔记(深入)”;
关键实操点:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 入队必须等
rdb.XAdd().Result()成功才返回,否则任务没存住 - 消费者组首次启动要用
XGROUP CREATE,否则XREADGROUP报NOGROUP No such key -
XACK一定要在业务逻辑执行完、状态更新成功后调用,提前 ACK = 任务丢失 - 用
XCLAIM处理超时未 ACK 的消息,代码里得能识别并重试或转入死信流
示例片段(简化版):
msgs, _ := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "email",
Consumer: "w1",
Streams: []string{"email_queue", ">"},
Count: 5,
Block: 5000,
}).Result()
for _, msg := range msgs[0].Messages {
if err := sendEmail(msg.Values["to"].(string)); err == nil {
rdb.XAck(ctx, "email_queue", "email", msg.ID)
} else {
rdb.XAdd(ctx, &redis.XAddArgs{Stream: "email_dlq", Values: msg.Values})
}
}
go-workers 和 gocraft/work 怎么选
两者都基于 Redis list(BRPOPLPUSH),但行为差异明显:
-
go-workers兼容 Sidekiq 协议,适合已有 Ruby/Sidekiq 生态的团队;重试靠workers.EnqueueWithOptions控制,但不原生支持死信队列,得自己补中间件 -
gocraft/work更 Go 风格,API 简洁,内置MaxRetries和RetryDelay,失败自动进work_failedlist,方便人工干预 - 两者都不支持消费者组语义,消息一旦被 pop 就从队列消失,若 worker panic 未 recover,任务直接丢失——这点比 Stream 方案脆弱
如果你已用 Redis 且不想升级版本,gocraft/work 上手更快;如果追求强可靠性、需精确控制重试与死信,优先上 Stream。
Worker 启动和生命周期管理最容易踩坑
不是起个 goroutine 循环读队列就完事,几个硬性要点:
- 用
sync.WaitGroup管理 worker 生命周期,但defer wg.Done()必须包在 recover 里,否则 panic 会导致 wg 永远不减,主程序卡死 - 所有阻塞操作(HTTP 调用、DB 查询)必须带
ctx,且主 ctx 要能穿透到最底层,否则超时无法中断 - DB 连接、HTTP client 必须复用,别在每个任务里 new —— 压测时会看到
net/http: request canceled (Client.Timeout exceeded while awaiting headers) - worker 进程退出前,要主动
QUIT消费者组或清理 pending 任务,否则重启后旧消息重复消费
复杂点不在怎么写逻辑,而在怎么让 worker 在崩溃、升级、扩容时不丢任务、不重复执行、不拖垮下游——这些细节,往往上线后第一波流量才暴露。

















