生产环境微服务异步调用必须用消息队列或Outbox模式,禁用goroutine直调HTTP;需保障持久化、重试、幂等、可观测性,并通过Router统一管理多Topic消费。

微服务里用 goroutine 直接调 HTTP 是伪异步,生产环境必须走消息队列或 Outbox 模式保数据不丢。
跨服务异步必须用消息队列,别裸起 goroutine 调 HTTP
常见错误现象:订单创建后 go http.Post 发短信,结果网络抖动导致请求失败,日志里查不到记录,用户收不到通知。
- goroutine + http.Client 只是“不阻塞当前协程”,但没重试、没持久化、没幂等、没可观测性
- RabbitMQ 用
amqp.Persistent确保消息落盘;Kafka 用kafka.WriteMessages后记得Flush()或调小BatchTimeout - NATS JetStream 默认 At-Least-Once,但消费端必须显式调
msg.Ack(),否则无限重发 - 事件结构体必须带
Version string `json:"version"`字段,后续升级靠它分流处理逻辑
本地异步任务要用 worker pool,不是无限制起 goroutine
HTTP handler 里随手写 go sendEmail() 是高危操作:goroutine 泛滥会吃光内存,panic 会杀死整个协程,且无法统计成功率。
- 用带缓冲的 channel 当信号量,例如
sem := make(chan struct{}, 10)控制最大并发数 - worker 启动前先往
sem写一个值,结束时读出,避免资源耗尽 - 别在 handler 里再发 HTTP 请求——这又绕回伪异步;应先落库,由另一个服务监听状态变更事件去发
- 轻量任务可用
sync.Pool复用对象,减少 GC 压力
需要结果返回时,别用全局 map 存 task_id → chan,改用独立 result channel
用 map[string]chan Result 配合定时清理,容易漏删、并发冲突、内存泄漏,而且无法绑定 context 生命周期。
立即学习“go语言免费学习笔记(深入)”;
- 每个任务生成唯一
taskID,并创建独立的resultCh := make(chan Result, 1) - goroutine 执行完后写入
resultCh,主协程用select+context.WithTimeout等待 - Redis 存状态时设
EX 3600自动过期,避免堆积无效 task_id - 对外提供
/v1/tasks/{id}查询接口,返回PENDING/SUCCESS/FAILED状态,不暴露内部 channel
Watermill Router 是多 topic 统一管理的关键,别手写一堆 Subscribe 循环
一个服务要同时消费 order.created、user.updated、payment.confirmed,如果每个 topic 都起一个 for-range goroutine,中间件重复注册、错误处理分散、metrics 难聚合。
- 用
router.AddHandler注册多个 handler,共用同一个 Subscriber 实例 - 每个 handler 的
subscribeTopic设对应 topic,publishTopic可为空(只消费)或设为下游 topic(转发) - handler 函数签名必须是
watermill.HandlerFunc,返回[]*watermill.Message才触发转发 - Kafka 消费者卡住?大概率是
session.timeout.ms小于 handler 执行时间,得调大并确保业务逻辑足够快
Outbox 模式和 Router 是最容易被跳过的两环:前者保证事件不丢,后者保证可维护性。跳过它们,系统上线后第一周就会开始查“为什么消息没了”和“为什么新加个 topic 就要改三处代码”。



















