不能用闭包实现通知分发器,因其无发布/订阅语义,易致并发写panic、goroutine泄漏、无法取消订阅及缺乏背压机制。

用闭包实现跨模块通知中枢是可行的,但容易误用成“伪事件总线”——表面解耦,实则埋下 goroutine 泄漏、状态污染和竞态隐患。
为什么不用闭包做通知分发器
闭包本身不是事件系统。它只是函数+捕获变量的组合体,不具备发布/订阅语义。常见错误是写一个 func() []chan interface{} 工厂函数,试图靠返回多个闭包来模拟通道注册,结果:
- 每个闭包都持有对同一 map 或 slice 的引用,多 goroutine 并发写导致 panic: concurrent map writes
- 闭包内启动的 goroutine 没有退出控制,服务长期运行后堆积大量空转协程
- 无法动态取消订阅,只能靠 channel 关闭,而关闭后的 channel 仍可读(返回零值),造成逻辑错乱
- 没有消息缓冲或背压机制,
Publish调用在select { case ch 时直接阻塞
闭包真正适合的通知场景
闭包该用在「封装配置 + 延迟绑定」环节,而不是「消息路由」本身。典型用法有:
- 为不同环境生成定制化
Notifier实例:比如devNotifier := newNotifier("localhost:8080", false)返回一个闭包,内部固化了调试开关和 mock 地址 - 构建带重试策略的发送函数:
sendWithRetry := newRetrySender(3, time.Second),闭包里捕获了重试次数和退避间隔 - 中间件式拦截:HTTP handler 中用闭包包裹原始 handler,注入 trace ID 和通知钩子,如
notifyOnFail(nextHandler, "order.created")
这些场景中,闭包只负责“记住参数”,不参与消息生命周期管理。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
轻量级通知中枢该怎么做
真要实现跨模块通信,应基于明确的事件模型,而非闭包拼凑。推荐用极简 map[string][]chan interface{} 配合以下约束:
- 所有
chan interface{}必须带缓冲:make(chan interface{}, 16),避免 publish 卡死 -
Subscribe返回取消函数,内部用sync.RWMutex安全删除 map 条目,不依赖 channel 关闭 - 每个订阅者自行启动消费 goroutine:
go func() { for msg := range ch { handle(msg) } }(),Broker 不承担执行责任 - 事件类型用字符串字面量(如
"user.registered"),避免用闭包生成动态类型名,增加调试难度
最容易被忽略的点
不是语法怎么写,而是谁负责清理。闭包捕获的变量若含 channel、timer、mutex 等资源,必须显式释放。例如一个通知重试闭包里启用了 time.Ticker,没在取消时 ticker.Stop(),就会持续触发 goroutine,且无法被 GC 回收。这种泄漏在压测时才暴露,但线上已积攒数小时。

















