结论:用 nats-go 客户端连 nats server,无需自实现队列逻辑——nats 是独立消息服务器,go 中只需连接、发布、订阅;自研易致内存泄漏、丢消息、无集群等严重问题。

直接说结论:用 nats-go 客户端连 NATS Server,不用自己实现队列逻辑 —— NATS 本身已是轻量级消息系统,Go 里只需做连接、发布、订阅三件事。
为什么不用“实现”队列,而要“使用”NATS
NATS 不是类库,是独立运行的消息服务器(nats-server)。它已内置主题路由、并发分发、客户端重连、JetStream 持久化等能力。你在 Go 里写的不是“消息队列”,而是“NATS 的生产者和消费者”。试图自己基于 channel 或 map 实现一个能对标 NATS 的队列,反而会踩内存泄漏、竞态、无持久化、无集群等坑。
- 常见错误现象:
panic: send on closed channel出现在自研队列的 goroutine 管理中;或消费者重启后丢消息,因为没对接持久层 - 真实使用场景:微服务间解耦(如订单服务 publish
order.created,库存服务 subscribe)、IoT 设备上报、实时告警推送 - 性能影响:自研纯内存队列可能吞吐高,但一崩溃就丢全量;而 NATS + JetStream 可在 20MB 内存下提供磁盘持久化,延迟仍保持在
nats.Connect() 连接时必须处理的 3 个现实问题
本地直连 "nats://localhost:4222" 在开发阶段没问题,进测试/生产立刻暴露问题:
- 超时控制缺失:默认连接无限等待,应显式设
nats.MaxReconnects(-1)(永久重试)+nats.ReconnectWait(2 * time.Second) - TLS 和认证被跳过:生产环境必须用
nats.UserCredentials("nats.creds")或nats.Token("xxx"),否则日志里全是未授权连接拒绝 - 单点风险:若用集群,URL 要写成逗号分隔列表,例如
"nats://n1:4222,nats://n2:4222",客户端自动负载均衡
示例片段:
nc, err := nats.Connect("nats://n1:4222,nats://n2:4222",
nats.Name("order-service"),
nats.MaxReconnects(-1),
nats.ReconnectWait(2*time.Second),
nats.UserCredentials("./creds/nats.creds"),
)
发布与订阅的语义差异直接影响可靠性
nc.Publish() 和 nc.Subscribe() 看似简单,但背后行为完全不同:
-
Publish()是“发完即弃”:不确认对方是否在线,不保证送达,适合监控、日志等允许丢失的场景 -
Subscribe()默认是“瞬时订阅”:进程退出即取消,且不保存离线消息 —— 若需断线重连后收到错过的消息,必须启用 JetStream 并用js.Subscribe(),指定DeliverPolicy(如DeliverAll) - 参数差异:普通
Subscribe()的回调函数签名是func(*nats.Msg);JetStream 的js.Subscribe()返回*nats.Subscription,支持Ack()、Nak()、InProgress()
关键区别代码示意:
// 普通 pub/sub(无状态、无重试)
nc.Publish("orders.new", []byte(`{"id":"123"}`))
<p>// JetStream 订阅(有状态、可重播、需 Ack)
js, <em> := nc.JetStream()
sub, </em> := js.Subscribe("orders.>", &nats.SubOpts{
DeliverPolicy: nats.DeliverAll,
AckPolicy: nats.AckExplicit,
})
for msg := range sub.Messages() {
process(msg)
msg.Ack() // 必须显式调用,否则下次还会收到
}</p>
JetStream 开启后,磁盘路径和配额容易被忽略
JetStream 默认把数据存在 /tmp/nats/jetstream,Docker 容器重启即清空。更危险的是,不设配额会导致磁盘写满:
- 启动 server 时必须加
-js参数,并用--store_dir /data/jetstream指向持久卷 - 创建 Stream 前,先检查配额:
js.AddStream(&nats.StreamConfig{...})中的MaxBytes(如10 * 1024 * 1024 * 1024)和MaxMsgs必须明确设置,否则默认不限制 - 容易踩的坑:本地开发用
nats-server -js启动,但忘了挂载 volume;上线后 Stream 创建失败,错误信息是"no storage available",而不是磁盘满
一句话收尾:NATS 的轻量,不在于代码行数少,而在于它把复杂性封装进一个二进制里;你在 Go 里真正要花精力的,是理解它的交付语义、连接韧性、以及 JetStream 的存储契约 —— 这些地方一旦配置错,比自己写队列还难排查。











