结论:跨服务或关键路径必须用持久化消息队列,本地轻量场景优先选 redis stream;goroutine 直接发 http 或调函数不具可靠性,仅适用于非关键、可丢失的内部埋点等场景。

直接说结论:别用 goroutine 直接发 HTTP 或调函数当“异步任务”,跨服务或关键路径必须走持久化消息队列;本地轻量场景优先选 Redis Stream,不是 Redis Pub/Sub;TaskQ 是封装好的轮子,但得清楚它底层仍依赖 Redis 或其他中间件。
为什么 go http.Post 不算真正的异步任务
现象是接口秒回,但下游服务挂了、网络超时、DNS 失败时,http.Post 会直接 panic 或返回 error,而你没 catch —— 任务就丢了。更糟的是,它不重试、不 ACK、不可追溯,日志里连“这条短信该不该重发”都查不到。
适用场景仅限:内部埋点上报、非关键通知(比如用户登录成功后发个站内信,丢了也不影响主流程)。
正确做法是把这类调用转成消息:
- 生产者写入 Redis Stream / Kafka Topic / RabbitMQ Exchange
- 消费者独立进程拉取、处理、XAck 或 msg.Ack()
- 失败时进死信队列(DLQ),人工干预或自动重试
Redis Stream 是最值得优先验证的轻量方案
它比 RabbitMQ 部署简单,比 Kafka 运维成本低,又比纯 channel / goroutine 多出持久化、消费者组、消息确认三大能力。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
容易踩的坑:
- XGROUP CREATE 必须在首次消费前执行,否则 XREADGROUP 报 NOGROUP No such key
- Streams: []string{"order_events", ">"} 中的 > 表示只读新消息;想重播历史消息得换 ID,比如 "0-0"
- XACK 一定要放在业务逻辑成功之后,不能写在 for 循环开头,否则消息一读就丢
- 消费者崩溃未 ACK 的消息,会被 XCLAIM 重新分配,代码里必须做幂等(例如用 task_id 写 SETNX)
TaskQ 能省事,但别当黑盒用
它封装了队列创建、序列化、重试、超时等逻辑,适合快速上线 MVP,但有几点必须知道:
- 它本身不提供存储,redisq.NewFactory() 依赖已初始化的 *redis.Client
- MainQueue.Add() 是同步写入 Redis Stream,失败会返回 error,你得处理(比如 fallback 到 DB 表暂存)
- 重试机制靠 taskq.TaskOptions.Retry 控制,但不会自动处理幂等;重复消费仍需业务层兜底
- 如果用内存队列(memqueue),进程重启任务全丢——只适合开发调试
goroutine + channel 适合什么场景
它不是“错”,只是适用边界极窄:单机、瞬时、无状态、失败可容忍、且总量可控。
典型例子:
- HTTP handler 中触发日志落盘(写文件或发 UDP 日志服务)
- 批量图片缩略图生成(任务数固定、内存能扛住、失败可重传)
- 埋点聚合计数(用 sync.Map 累加,最后定时 flush)
必须加的防护:
- defer func() { recover() }() 防止 panic 杀掉整个 goroutine
- channel 设缓冲(如 make(chan Task, 100)),避免发送方阻塞
- worker 启动前 wg.Add(1),结束时 wg.Done(),主流程用 wg.Wait() 收口
- 别在闭包里直接引用循环变量,要显式传参:go func(id int) { ... }(i)
真正难的从来不是“怎么发消息”,而是“怎么确保每条消息都被处理一次且仅一次”。Redis Stream 的 XCLAIM、RabbitMQ 的 DLX、Kafka 的 offset commit,都是为这事服务的。选型时先问自己:这条任务丢了,用户会不会打电话来?
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










