直接用 net/http 做服务间 Pub/Sub 会出问题,因其请求-响应模型不支持服务端主动推送,强行轮询或长连接易导致连接混乱、消息丢失、重复消费和无序投递;微服务需解耦、容错与水平扩展,必须引入轻量级消息中间件,推荐 NATS——无依赖、启动快、协议简单,nats.go 客户端稳定直观,比 Kafka 或 RabbitMQ 更适合中小规模起步。

为什么直接用 net/http 做服务间 Pub/Sub 会出问题
HTTP 是请求-响应模型,天然不支持服务端主动推送。如果强行用轮询或长连接模拟订阅,会迅速暴露连接管理混乱、消息丢失、重复消费、无序投递等问题。微服务场景下,更需要解耦、容错和水平扩展能力,这不是 HTTP 客户端能兜住的。
真正可行的路径是引入轻量级消息中间件,让 Go 服务只专注收发逻辑。推荐从 NATS 入手——它无依赖、启动快、协议简单,nats.go 客户端稳定且 API 直观,比 Kafka 或 RabbitMQ 更适合中小规模微服务起步阶段。
nats.Connect() 连接失败的三个高频原因
不是网络不通才连不上。很多团队卡在认证、TLS 和重连策略上:
-
NATS默认开启用户凭据认证(--user/--pass),但 Go 客户端不自动读取环境变量,必须显式传入nats.UserInfo("u", "p") - 若 NATS 启用了 TLS(如
nats://localhost:4222改为tls://localhost:4222),需额外提供nats.Secure(&tls.Config{InsecureSkipVerify: true}),否则报tls: first record does not look like a TLS handshake - 默认重连间隔是 2 秒,连续失败 10 次后放弃。生产环境建议用
nats.MaxReconnects(-1)(无限重试)+nats.ReconnectWait(5 * time.Second)
用 nc.Subscribe() 实现可靠订阅的关键配置
单纯调 Subscribe 只能收到实时消息,服务重启就会丢历史事件。要保障至少一次投递(at-least-once),必须配合持久化队列和确认机制:
立即学习“go语言免费学习笔记(深入)”;
- 使用
QueueSubscribe而非Subscribe,并传入唯一队列名(如"order-processor-q"),让 NATS 自动做负载均衡和去重 - 消息处理函数内必须显式调用
msg.Ack(),否则 NATS 会在超时(默认 30 秒)后重发;若处理失败,改用msg.Nak()触发立即重试 - 避免在回调里做阻塞操作(如同步 HTTP 请求),应把耗时逻辑扔进 goroutine 并用带缓冲 channel 控制并发,否则会卡死整个 subscription 的消息循环
示例片段:
_, err := nc.QueueSubscribe("orders.created", "order-processor-q", func(msg *nats.Msg) {
go func() {
if err := processOrder(msg.Data); err != nil {
msg.Nak()
return
}
msg.Ack()
}()
})
nc.Publish() 发布时要注意的序列化与主题设计
NATS 不管数据格式,但微服务间契约一旦松动就难收敛。发布前务必统一两点:
- 所有事件结构体导出字段必须加 JSON 标签(如
type OrderCreated struct { ID string `json:"id"` }),否则json.Marshal输出空对象 - 主题名别用硬编码字符串,定义为常量(如
const TopicOrderCreated = "orders.created"),并在服务启动时用nc.Flush()验证能否成功发布(可捕获nats.ErrConnectionClosed等早期错误) - 不要在主题中嵌入服务名(如
"payment-service.orders.created"),而应按业务域分层("orders.created"、"orders.shipped"),靠订阅方自行决定是否关注——这才能支撑未来服务拆分
发布本身很简单:
data, _ := json.Marshal(OrderCreated{ID: "123"})
err := nc.Publish(TopicOrderCreated, data)
但真正麻烦的是后续:谁负责消息幂等?谁清理过期事件?这些不会因为用了 NATS 就自动消失。主题设计越早收敛,后期补救成本越低。


















