用channel+map实现pub/sub需以map[string][]chan interface{}为核心结构,每个订阅者独占带缓冲channel,broker用sync.rwmutex保护映射,publish异步非阻塞投递,subscribe返回channel由调用方自行启goroutine消费,并提供unsubscribe安全清理资源。

用 channel + map 实现基础发布订阅,别直接裸写 goroutine
goroutine 本身不提供发布订阅能力,必须配合 channel 和数据结构(比如 map[string][]chan interface{})来组织。直接起一堆 goroutine 盲发消息,既无法路由,也无法取消监听,很快就会泄漏 goroutine 和 channel。
核心思路是:一个中心 broker 管理主题(topic)到监听者 channel 的映射,所有 publish 都走它,所有 subscribe 都注册自己的接收 channel;每个监听者自己启 goroutine 从 channel 读,避免阻塞 broker。
-
subscribe返回一个chan interface{},调用方负责启动 goroutine 消费,比如go func() { for v := range ch { ... } }() - 注册时用
sync.RWMutex保护map,读多写少场景下比sync.Mutex更合适 - 务必提供
unsubscribe接口,否则 map 中的 channel 引用永远不释放,监听者退出后变成“幽灵订阅”
为什么不能用 unbuffered channel 做 subscriber channel?
如果给每个订阅者分配一个 make(chan interface{})(即无缓冲),publish 时往这个 channel 发送会立刻阻塞,直到有人在另一端 receive——但你无法保证监听 goroutine 一定在运行、没 panic、没卡住。一旦某个 subscriber channel 阻塞,整个 publish 流程就卡死,其他正常订阅者也收不到消息。
正确做法是用带缓冲的 channel:make(chan interface{}, 16)。缓冲大小要权衡:太小容易丢消息(发送时缓冲满且没人及时读),太大则内存占用不可控。生产环境建议根据 QPS 和平均处理延迟估算,初期可设为 64 或 128。
- 缓冲区满时,
select+default可实现非阻塞发送,丢弃或告警,避免阻塞 broker - 不要用
len(ch) == cap(ch)判断是否满——这只能反映快照,竞态下不准;应靠select逻辑控制 - Go 1.22+ 支持
chan<t></t>类型推导,但缓冲大小仍需显式传参,别漏掉
如何安全关闭 subscriber channel 并清理 map 条目?
关闭 channel 本身不难(close(ch)),难点在于:关闭时机、并发清理 map、防止重复关闭。常见错误是 unsubscribe 里直接 close(ch) 后立刻从 map 删除,但此时可能还有 goroutine 正在 for range ch,导致 panic: “send on closed channel” 或 “close of closed channel”。
推荐方案:让监听 goroutine 自己决定何时退出,并通知 broker 清理。例如,subscribe 同时返回一个 done chan struct{},监听方在退出前 close 它;broker 启一个 goroutine 监听所有 done,收到信号后再安全 close subscriber channel 并删 map 条目。
- 避免在
unsubscribe函数内直接close(ch),除非你能确保没有其他 goroutine 在读它 - map 删除操作必须和
publish使用同一把锁,否则遍历 map 时可能 panic: “concurrent map read and map write” - 测试时用
runtime.GC()+debug.ReadGCStats()观察 goroutine 数量变化,确认无泄漏
要不要用第三方库?看场景再定
标准库能搞定,但轮子已很成熟:github.com/google/uuid 配合 github.com/ThreeDotsLabs/watermill 或轻量级的 github.com/bsm/sarama(仅 Kafka)属于重型;更贴近需求的是 github.com/oklog/ulid + 手写,或者直接用 github.com/robfig/cron 风格的 github.com/segmentio/kafka-go ——但它们都引入了额外依赖和抽象层。
如果你只做进程内通信、主题数
真正容易被忽略的不是语法,而是状态生命周期管理:channel 关闭、goroutine 退出、map 条目清理、错误传播路径——这些点任何一个没对齐,都会在高负载下突然崩掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











