sync.map适用于读多写少、键动态增删的高并发场景,因无锁读性能更稳,但遍历时不可删除,需先拷贝键列表再广播;取消订阅必须用loadanddelete,发布广播应通过带缓冲channel异步解耦。

为什么直接用 sync.Map 而不是 map + sync.RWMutex?
因为订阅者列表在运行时频繁增删,且读多写少,sync.Map 在高并发场景下避免了全局锁争用,性能更稳。但要注意:sync.Map 不支持遍历中删除,所以广播消息时必须先拷贝键列表,再逐个取值调用回调——否则会 panic 或漏通知。
常见错误现象:fatal error: concurrent map iteration and map write,就是没做键拷贝直接边遍历边从 sync.Map 里删订阅者(比如取消订阅逻辑和广播混在一起触发)。
- 订阅时用
Store(subID, callback) - 广播前用
Range遍历并 append 到临时 slice,不操作原 map - 取消订阅必须用
LoadAndDelete,不能只Load后手动删——那是竞态源头
如何安全地在 goroutine 中广播消息而不阻塞发布者?
发布者调用 Publish 时不应被某个慢订阅者拖住。典型做法是把消息分发逻辑扔进新 goroutine,但必须控制并发量,否则海量消息可能压爆内存。
使用带缓冲的 channel 做轻量级异步队列最稳妥:启动一个长期运行的 dispatcher goroutine,从 channel 拉消息、执行广播;发布者只负责往 channel 写,写满就非阻塞丢弃或返回错误(根据业务容忍度选)。
- 缓冲区大小建议设为
100–1000,太小易阻塞,太大易积压 - dispatcher 里对每个订阅者调用 callback 前加
select { case ,支持整体超时/取消 - 不要用
go f(msg)每次都起 goroutine——无节制 spawn 是 goroutine 泄漏主因
Subscribe 返回的 unsubscribe 函数为什么必须是闭包?
因为要捕获当时生成的唯一 subID,并在内部调用 subs.LoadAndDelete(subID)。如果只是返回一个固定函数,所有取消操作都会删错 key。
容易踩的坑:用 defer unsubscribe() 在 handler 里注册取消,结果 handler 还没执行完就提前解除了订阅——比如 HTTP handler 中 defer 写在开头,但实际业务逻辑需要持续接收后续消息。
- 正确模式:
subID := uuid.NewString()+subs.Store(subID, cb)+ 返回func(){ subs.LoadAndDelete(subID) } - 测试时可故意让 callback sleep 3s,验证多次
unsubscribe()调用是否只生效一次 - 别把
unsubscribe存到 struct 字段里复用——不同订阅实例必须隔离 subID
怎么处理订阅者 panic 导致整个广播流程中断?
一个订阅者 callback panic,若不 recover,dispatcher goroutine 会退出,后续消息全丢。必须在单次回调调用处兜底。
不能在 dispatcher 外层用 recover——那只能救一次;必须在每次 cb(msg) 前加 defer func(){ recover() }(),确保单个失败不影响其他订阅者。
- 建议封装成工具函数:
safeCall(cb, msg),内部做 recover 并 log 错误栈 - panic 日志里至少带上
subID和msg的类型/长度,方便定位是哪个订阅者写的烂代码 - 别忽略 recover 后的错误——至少打 warn 级日志,否则问题静默发生
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











