用 channel 实现基础 Pub/Sub 易致内存泄漏,因订阅者退出未通知发布者,导致 goroutine 和 channel 无法释放,select 中的 case ch 阻塞或泄漏。
用
实现基础 Pub/Sub 容易内存泄漏
Go 原生没有内置 Pub/Sub,很多人第一反应是用
+ goroutine 转发,但这样写极易导致 goroutine 泄漏或 channel 阻塞。核心问题是:订阅者退出时没通知发布者,
里的
实操建议:
别直接把
暴露给订阅者,改用带取消能力的
控制生命周期
每个订阅者应持有独立的
,由中心分发器统一读取并
写入,避免多个 goroutine 同时往一个 channel 写
分发逻辑必须加超时或非阻塞写(如
示例关键片段:
存订阅者比
+
更适合高频增删
当订阅者频繁注册/注销(比如 WebSocket 连接进出),用普通
加
容易成为瓶颈——每次读都要抢锁,而
对读多写少场景做了优化,且支持原子删除。
注意点:
立即学习
“
go语言免费学习笔记(深入)
”;
的
/
接口只接受
,不能直接存闭包或未导出结构体;推荐封装成带 ID 的订阅者结构体
别在
回调里对
做写操作,会 panic;遍历前先用
拷贝快照
如果订阅者数量稳定(map +
更轻量,
的优势在千级并发订阅场景
用
管理订阅生命周期比手动关 channel 更可靠
常见错误是让订阅者自己关掉接收 channel,结果发布端还在往已关闭的 channel 发送,触发 panic:
。根本原因是关闭时机不可控——谁关、何时关、是否重复关,全靠约定。
正确做法是把控制权交给 context:
go语言参考手册 中文CHM版
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译
下载
订阅时返回
和
,由调用方决定何时取消
发布端用
监听取消信号,自动从订阅列表移除该订阅者
避免在 goroutine 里 defer 关 channel——万一 context 先取消,defer 还是会执行,造成 double-close
示例:
}
不要用
做泛型事件分发,类型断言足够且更安全
有人想支持“不同事件类型走不同 handler”,就用
匹配 handler map。这不仅慢(反射开销大),还绕过了编译期类型检查,运行时容易 panic。
更务实的做法:
定义顶层事件接口
,所有事件实现它
发布时传
,订阅时按
字符串过滤,用
或 map[string]func(Event) 分发
需要强类型处理?用类型断言:
,清晰、可测、无反射成本
性能差异明显:纯类型断言比反射快 5–10 倍,且 IDE 能跳转、go vet 能检查。
复杂点在于订阅者退出与消息投递的竞态——哪怕用了 context,仍需确保分发循环不因某个订阅者 channel 关闭而中断。最容易被忽略的是:没给订阅 channel 设置缓冲区大小,导致首条消息就阻塞整个分发流程。
channelchan interface{}selectcase ch 可能永远卡住,对应 goroutine 就再也退不出去。chancontext.Contextchanselectselect { case ch ),否则一个慢订阅者拖垮全部func (p *PubSub) publish(msg interface{}) {
for _, ch := range p.subs {
select {
case ch <- msg:
default:
// 订阅者太慢,跳过,不阻塞
}
}
}sync.Mapmapmutexmapsync.RWMutexsync.Mapsync.MapLoadStoreinterface{}Rangesync.MapLoadAllRWMutexsync.Mapcontext.WithCancelsend on closed channelchan interface{}context.CancelFuncctx.Done()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)
go func() {
<-ctx.Done()
p.subs.Delete(id)
close(ch)
}()
return chreflectreflect.TypeOf(msg)type Event interface{ Topic() string }EventTopicswitchif e, ok := msg.(UserCreatedEvent); ok { ... }