用 sync.map 实现轻量级 pub/sub,适合单机多协程场景:以 topic 为 key、[]chan interface{} 为 value 存储订阅通道,由订阅者自行关闭专属通道,发布时遍历切片并 select 发送防 panic。

用 sync.Map 实现轻量级 Pub/Sub,适合单机多协程场景
Go 原生没有内置发布订阅组件,但用 sync.Map 搭配通道(chan)就能快速搭出线程安全的简单 Pub/Sub。关键不是“造轮子”,而是控制住 goroutine 泄漏和内存泄漏。
常见错误是直接把 chan interface{} 存进 map,却不提供关闭机制——订阅者退出后通道还挂着,发布者一发消息就 panic:send on closed channel。
- 每个订阅者应持有自己专属的
chan interface{},且由订阅者负责关闭 -
sync.Map存的是map[string][]chan interface{}结构,key 是 topic,value 是该 topic 下所有活跃订阅通道的切片 - 发布时遍历通道切片,用
select { case ch 非阻塞发送,避免因某个订阅者卡住拖垮整个发布流程 - 务必提供
Unsubscribe(topic, ch)方法,内部用sync.Map.Load/Store做原子更新,不能直接修改切片
用 context.Context 管理订阅生命周期,防止 goroutine 泄漏
很多实现只管“注册通道”,不管“谁来关它”。结果是 HTTP handler 启动一个订阅,handler 返回了,goroutine 还在等消息——内存和 goroutine 数缓慢上涨。
正确做法是让订阅返回一个取消函数,或直接绑定 context.Context:
- 订阅方法签名建议为
Subscribe(ctx context.Context, topic string) ( - 返回的取消函数内部调用
context.CancelFunc并触发Unsubscribe - 接收循环必须用
for { select { case ,不可忽略 <code>ctx.Done() - 别把
http.Request.Context()直接传给后台长期运行的订阅逻辑——它的生命周期太短,容易误关
需要持久化或跨进程?别硬写,直接用 redis.PubSubConn 或 nats.go
一旦涉及服务重启后消息不丢、多个实例共享 topic、或需要通配符订阅(logs.*),纯内存方案立刻失效。这时候自己基于 TCP 或文件做持久化,大概率不如直接集成成熟方案。
Redis 的 Pub/Sub 低延迟、够简单,github.com/go-redis/redis/v9 的 PubSubConn 支持重连和消息确认;NATS 更适合微服务间解耦,nats.go 客户端对 Go 友好,支持 JetStream 持久化。
- Redis Pub/Sub 不保证消息可达(断连期间发布的消息丢失),适合实时通知类场景
- NATS 默认也是 at-most-once,启用 JetStream 后可做到 exactly-once,但需额外部署和配置
- 不要试图用 SQLite 或本地文件模拟 broker——并发写入、锁竞争、崩溃恢复全是坑
测试时最容易漏掉的:并发写入 sync.Map 的 key 冲突与重复订阅
测试常只跑单个 topic + 单个订阅者,上线后突然出现重复消费或某 topic 完全收不到消息——大概率是并发调用 Subscribe 时,两个 goroutine 同时执行 load → append → store,后写的覆盖了前写的切片。
必须用 sync.Map.LoadOrStore + atomic.Value 或互斥锁保护切片操作:
// 错误:竞态
chans, _ := m.Load(topic)
newChans := append(chans.([]chan interface{}), ch)
m.Store(topic, newChans)
<p>// 正确:用 LoadOrStore 配合锁或 CAS 更新
m.LoadOrStore(topic, &sync.Map{}) // 确保 key 存在
topicMap, _ := m.Load(topic).(*sync.Map)
topicMap.Store(subID, ch) // subID 可以是 uintptr(unsafe.Pointer(&ch))
</p>
另外,测试要覆盖重复调用 Subscribe("user.created", ch) 多次的情况——同一通道被加进切片多次,导致一条消息发三遍。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











