最简发布订阅用map+chan实现,以主题为key、带缓冲通道为value,配RWMutex保护并发;发布时快照通道列表并select非阻塞发送;订阅需返回取消函数清理map和通道,主题应语义化而非UUID。

用 map + chan 实现最简发布订阅
核心就是维护一个主题(string)到订阅者通道(chan interface{})的映射,发布时遍历所有对应通道发送消息。注意:不能直接在发布时向 chan 写入而不做保护——如果某个订阅者没及时读,写操作会阻塞整个发布流程。
实操建议:
- 每个订阅者应启动独立 goroutine 消费自己的
chan,避免阻塞发布者 - 使用
sync.RWMutex保护map的并发读写,尤其在动态增删订阅时 - 通道建议带缓冲(如
make(chan interface{}, 1)),防止单个慢消费者拖垮全局 - 不推荐用无缓冲通道做订阅通道,极易因消费滞后导致发布卡死
sync.Map 能替代普通 map 吗?
可以,但不推荐作为首选。虽然 sync.Map 免去了显式加锁,但它不支持遍历——而发布动作必须“找到所有监听该主题的通道”,这就必须能枚举键值对。sync.Map 的 Range 是快照式遍历,期间新增/删除订阅可能被跳过;且无法保证遍历与写入的严格一致性。
更稳妥的做法仍是 map 配 sync.RWMutex,并在读取前加读锁、写入时加写锁:
立即学习“go语言免费学习笔记(深入)”;
mu.RLock()
chans := make([]chan interface{}, 0, len(m[topic]))
for _, ch := range m[topic] {
chans = append(chans, ch)
}
mu.RUnlock()
for _, ch := range chans {
select {
case ch <- msg:
default: // 避免阻塞,丢弃或记录
}
}
如何安全关闭订阅通道并清理资源
单纯关闭 chan 不足以清理,因为已关闭的通道仍可读(返回零值),且 map 中残留的 nil 或已关闭通道会导致后续发布 panic 或逻辑错乱。
1、数据调用该功能使界面与程序分离实施变得更加容易,美工无需任何编程基础即可完成数据调用操作。2、交互设计该功能可以方便的为栏目提供个性化性息功能及交互功能,为产品栏目添加产品颜色尺寸等属性或简单的留言和订单功能无需另外开发模块。3、静态生成触发式静态生成。4、友好URL设置网页路径变得更加友好5、多语言设计1)UTF8国际编码; 2)理论上可以承担一个任意多语言的网站版本。6、缓存机制减轻服务器
关键动作是「移出 map」+「通知消费者退出」:
- 不要在发布逻辑里检查
cap(ch) == 0或ch == nil来跳过,这不可靠 - 订阅函数应返回一个
func()取消函数,内部完成:从map删除通道 + 关闭通道 + 唤醒消费者 goroutine - 消费者 goroutine 应用
for msg := range ch模式,通道关闭后自动退出 - 若需等待消费者真正退出,可用
sync.WaitGroup计数,但注意别在持有锁时wg.Wait()
为什么不用第三方库如 github.com/google/uuid 生成主题名
主题名本质是业务语义标识(如 "user.created"、"order.paid"),不是为了唯一性。用 UUID 当主题名反而让调试和监控变困难——你没法一眼看出事件类型,日志里全是随机字符串,Prometheus 标签也难以聚合。
真正需要唯一性的场景是「临时请求响应匹配」(比如 RPC 回调),那属于请求-响应模式,不是发布订阅。发布订阅的主题应是稳定、可读、可预测的字符串常量或拼接结果。
容易被忽略的一点:主题层级设计会影响扩展性。例如用 "payment.usd.success" 而非 "payment_usd_success",后续就能支持通配符订阅(如 "payment.*.success"),但 Go 标准库不内置通配匹配,得自己实现或引入轻量库如 github.com/robfig/pat 做前缀匹配。









