不可行,因闭包函数无法安全支持动态增删、阻塞回调易导致panic传播和goroutine泄漏;sync.Map不保护value内部结构,并发操作切片仍会panic,必须用RWMutex封装callback列表并为每个订阅分配唯一ID以支持可靠取消。

直接用闭包函数实现 Pub/Sub 在 Go 里可行,但只适合启动期静态注册、无动态取消、低频发布的场景;一旦订阅者带阻塞操作或需要运行时增删,就会立刻暴露 goroutine 泄漏、panic 传播、并发写 panic 等问题。
为什么 sync.Map + callback 切片必须配 RWMutex
很多人以为用 sync.Map 就万事大吉,结果上线后偶发 panic: “concurrent map writes” 或回调漏执行。根本原因是:sync.Map 只保证对 key 的原子读写,不保护 value 内部结构——比如你存的是 *subList,而 subList.fns 是个切片,多个 goroutine 同时 append 它,照样崩。
- 必须把 callback 列表封装成独立结构体,内嵌
sync.RWMutex -
Subscribe时调用add()方法加写锁,Publish时用读锁遍历调用 - 别在
sync.Map.LoadOrStore返回的 value 上直接做切片操作,那是裸奔
闭包回调里不加 recover 和超时就是埋雷
一个 func(interface{}) 订阅后,如果内部调用 http.Get 卡住、或没处理 nil panic,整个 Publish 循环就断了——后续所有订阅者都收不到消息。
- 每个回调必须包裹
defer func() { recover() }(),且至少打日志:例如log.Printf("sub %p panic: %v", fn, r) - 耗时操作(DB/HTTP)必须自带 context 超时,例如
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - 禁止在
Publish中统一加 timeout —— 会中断已开始执行的回调,无法判断哪些成功了
Unsubscribe 失效几乎总是函数比较问题
调了 Unsubscribe("user.created", handler),但 handler 还在执行?不是逻辑写错了,是 Go 里函数值不能用 == 比较。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 匿名函数、方法值、闭包即使代码完全一样,地址也不同,
==返回false - 解决办法:给每个订阅绑定唯一 ID(如
uuid.NewString()),Subscribe返回 ID,Unsubscribe传 ID 删除 - 别存函数本身到 map,改存
struct{ id string; fn func(interface{}) } - 如果真要用函数值作 key,得用
unsafe.Pointer(reflect.ValueOf(fn).Pointer()),但极其不推荐——反射+unsafe 在生产环境等于放弃可维护性
缓冲 channel 不是万能解药,别让它变成背压黑洞
有人觉得“加个 make(chan interface{}, 100) 就稳了”,结果线上发现内存持续上涨,pprof 显示一堆 goroutine 堵在 ch 。这是因为缓冲只是延缓问题,不是解决消费慢。
- 缓冲 channel 只适用于消费延迟稳定、且有明确上限的场景(比如日志采集)
- 若订阅者处理时间波动大(如依赖外部 API),缓冲只会掩盖问题,最终积压溢出
- 真正要做的:在
select { case ch 分支里记录丢弃数,并告警——丢消息比卡死更可控 - 别让 Broker 承担消费责任;谁
Subscribe,谁负责开 goroutine 读ch并处理,Broker 只管投递
闭包模型最脆弱的地方不在语法,而在生命周期管理:函数引用可能捕获外部变量导致内存无法释放,goroutine 可能因未设超时而永久挂起,取消机制若依赖值比较就注定失效。这些都不是“写完能跑”就能绕过的坑。

















