用 channel 实现基础 Pub/Sub 易致内存泄漏,因订阅者退出未通知发布者,导致 goroutine 和 channel 无法释放,select 中的 case ch 阻塞或泄漏。

用 channel 实现基础 Pub/Sub 容易内存泄漏
Go 原生没有内置 Pub/Sub,很多人第一反应是用 chan interface{} + goroutine 转发,但这样写极易导致 goroutine 泄漏或 channel 阻塞。核心问题是:订阅者退出时没通知发布者,select 里的 case ch 可能永远卡住,对应 goroutine 就再也退不出去。
实操建议:
- 别直接把
chan暴露给订阅者,改用带取消能力的context.Context控制生命周期 - 每个订阅者应持有独立的
chan,由中心分发器统一读取并select写入,避免多个 goroutine 同时往一个 channel 写 - 分发逻辑必须加超时或非阻塞写(如
select { case ch ),否则一个慢订阅者拖垮全部
示例关键片段:
func (p *PubSub) publish(msg interface{}) {
for _, ch := range p.subs {
select {
case ch <- msg:
default:
// 订阅者太慢,跳过,不阻塞
}
}
}
sync.Map 存订阅者比 map + mutex 更适合高频增删
当订阅者频繁注册/注销(比如 WebSocket 连接进出),用普通 map 加 sync.RWMutex 容易成为瓶颈——每次读都要抢锁,而 sync.Map 对读多写少场景做了优化,且支持原子删除。
注意点:
立即学习“go语言免费学习笔记(深入)”;
-
sync.Map的Load/Store接口只接受interface{},不能直接存闭包或未导出结构体;推荐封装成带 ID 的订阅者结构体 - 别在
Range回调里对sync.Map做写操作,会 panic;遍历前先用LoadAll拷贝快照 - 如果订阅者数量稳定(map +
RWMutex更轻量,sync.Map的优势在千级并发订阅场景
用 context.WithCancel 管理订阅生命周期比手动关 channel 更可靠
常见错误是让订阅者自己关掉接收 channel,结果发布端还在往已关闭的 channel 发送,触发 panic:send on closed channel。根本原因是关闭时机不可控——谁关、何时关、是否重复关,全靠约定。
正确做法是把控制权交给 context:
- 订阅时返回
chan interface{}和context.CancelFunc,由调用方决定何时取消 - 发布端用
ctx.Done()监听取消信号,自动从订阅列表移除该订阅者 - 避免在 goroutine 里 defer 关 channel——万一 context 先取消,defer 还是会执行,造成 double-close
示例:
func (p *PubSub) Subscribe(ctx context.Context) <-chan interface{} {
ch := make(chan interface{}, 16)
id := atomic.AddUint64(&p.nextID, 1)
p.subs.Store(id, ch)
<pre class='brush:php;toolbar:false;'>go func() {
<-ctx.Done()
p.subs.Delete(id)
close(ch)
}()
return ch
}
不要用 reflect 做泛型事件分发,类型断言足够且更安全
有人想支持“不同事件类型走不同 handler”,就用 reflect.TypeOf(msg) 匹配 handler map。这不仅慢(反射开销大),还绕过了编译期类型检查,运行时容易 panic。
更务实的做法:
- 定义顶层事件接口
type Event interface{ Topic() string },所有事件实现它 - 发布时传
Event,订阅时按Topic字符串过滤,用switch或 map[string]func(Event) 分发 - 需要强类型处理?用类型断言:
if e, ok := msg.(UserCreatedEvent); ok { ... },清晰、可测、无反射成本
性能差异明显:纯类型断言比反射快 5–10 倍,且 IDE 能跳转、go vet 能检查。
复杂点在于订阅者退出与消息投递的竞态——哪怕用了 context,仍需确保分发循环不因某个订阅者 channel 关闭而中断。最容易被忽略的是:没给订阅 channel 设置缓冲区大小,导致首条消息就阻塞整个分发流程。











