直接用 sync.map 是因它原生并发安全,避免 map + rwmutex 在遍历删除时 panic,且读多写少性能更稳;range 是快照语义,适合发布订阅场景。
为什么直接用 sync.map 而不是自己加锁的 map?
因为发布订阅的核心是高频并发读写:多个 goroutine 可能同时 publish 事件,也常有多个 subscribe 在注册或取消。自己用 map + sync.rwmutex 容易在删除回调时触发 panic —— 比如遍历中删 key,或读写竞争导致 map 迭代器失效。而 sync.map 原生支持并发安全的增删查,且读多写少场景下性能更稳。
注意:sync.Map 的 Range 是快照语义,遍历时新增的 subscriber 不会被本次通知覆盖,这反而是合理行为;但如果你需要强一致性广播(比如必须通知到“此刻所有活跃 subscriber”),就得换方案(如用 channel 配合注册中心)。
- 别把
sync.Map当普通 map 用:Load/Store是必须的,map[key] = val会编译失败 - value 类型建议封装为
chan interface{}或自定义结构体,避免裸存函数指针(不利于生命周期管理) - 如果 topic 数量极少(map[string][]func(interface{}) + 全局 mutex 更直观,不用过度设计
如何安全地实现 Unsubscribe 并防止 goroutine 泄漏?
常见错误是只从 map 中删掉回调函数,却不关掉对应的接收 channel —— 尤其当使用 chan interface{} 做消息管道时,未关闭的 channel 会让 goroutine 一直阻塞在 上,最终堆积泄漏。
正确做法是:在 Unsubscribe 里不仅删 map 条目,还要显式 close 对应的 channel,并确保消费者 goroutine 能检测到关闭信号后退出。
- 推荐为每个 subscriber 启动独立 goroutine,用
for msg := range ch循环接收,这样close(ch)后循环自动结束 - 不要在
Subscribe返回的函数里直接启动 goroutine 处理消息(难以控制生命周期),应返回一个可被外部管理的unsubscribe函数 - 如果用函数切片存储 handler,
Unsubscribe需要比较函数地址(unsafe.Pointer)或用唯一 ID 标识,否则无法精准移除
Publish 时要不要做深度拷贝或同步等待?
默认不建议。Golang 的 pub/sub 本质是解耦,Publish 应该快速返回,避免因某个慢 subscriber 拖垮整个系统。所以典型实现是:遍历当前 topic 的所有 subscriber,对每个 chan 或 handler 异步调用(go f(msg))或非阻塞发送(select { case ch )。
但要注意:如果 msg 是可变结构体指针(如 *User),多个 subscriber 并发修改它会导致数据竞争;如果 msg 是大对象,反复传值可能引发 GC 压力。
- 传值优先:小结构体或
interface{}包裹的不可变值(如string,int,struct{ID int; Name string})直接传 - 传指针需谨慎:仅当明确所有 subscriber 都只读,且 publisher 不再修改该对象;否则用
proto.Clone或手动 copy - 绝对不要在
Publish里用sync.WaitGroup等待所有 subscriber 执行完——这违背了 pub/sub 的异步契约
用 context.Context 控制订阅生命周期是否必要?
有必要,尤其当 subscriber 是临时任务(如 HTTP 请求处理期间监听日志事件)或需要超时自动退订时。context 不只是传递取消信号,还能统一管理 goroutine 的退出、资源清理和超时逻辑。
例如,在 Subscribe 中接收 ctx 参数,内部启动 goroutine 监听 ctx.Done(),一旦触发就执行 Unsubscribe 并关闭 channel。这样上层不用手动调 Unsubscribe,也不怕忘记清理。
- 别把
context.Background()当默认值传进Subscribe—— 它永远不会 cancel,等于放弃生命周期管理 - 如果 subscriber 本身要发起网络请求,把
ctx透传给下游 http.Client 是自然延伸,不是额外负担 -
context.WithCancel创建的子 context 最适合做单次订阅控制;context.WithTimeout适合“监听 5 秒内首次事件”这类场景
实际跑起来最易忽略的是:topic 名字的拼写一致性(大小写、空格、前缀)、subscriber 的 panic 捕获(一个 handler panic 会 kill 整个 publish 循环)、以及测试时没 mock 时间相关逻辑(比如用 time.AfterFunc 做延迟 publish)。这些细节比模式本身更决定落地成败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











