跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

如何在Golang中实现观察者模式Observer Go语言事件发布订阅机制

用 sync.Map 实现线程安全事件注册表,适合读多写少场景;注册用 Store,触发前 Load 后遍历调用 handler,禁止遍历中 Delete;需动态注销应改用带锁自定义结构;默认同步执行 handler,异步需用 worker pool 控制并发。 用
sync.Map
实现线程安全的事件注册表 Go 原生没有内置的 Observer 接口,直接用 map 存订阅者会 panic:「concurrent map read and map write」。必须自己加锁或选并发安全结构。
sync.Map
是最轻量的选择,适合读多写少的事件场景(比如配置变更、状态通知),但注意它不支持遍历中删除——触发事件时不能边遍历边取消订阅。 注册用
Store(key, value)
,key 通常用
string
类型的事件名,value 是
[]func(interface{})
切片 触发前用
Load(key)
拿到 handler 列表,再遍历调用;别在遍历中调
Delete()
如果需要支持动态注销,改用带互斥锁的自定义结构,而不是强塞
sync.Map
避免 goroutine 泄漏:事件回调里别直接起
go func()
常见错误是每个事件都开 goroutine 异步执行 handler,结果 handler 执行慢或阻塞,导致 goroutine 积压。尤其在高频事件(如日志推送、心跳)下,几秒就上千个 goroutine。 默认同步执行 handler,让调用方决定是否异步——更可控 真要异步,统一用 worker pool 控制并发数,比如用
chan func()
+ 固定数量的 goroutine 消费 handler 内部若含网络调用,务必设超时,否则整个事件分发链会被拖住
interface{}
传参太松散?用具体事件类型替代 很多示例用
func(event interface{})
当 handler 签名,看似灵活,实际导致类型断言泛滥、运行时报错难定位。比如发的是
*UserCreatedEvent
,但某个 handler 错写成
event.(*OrderEvent)
,一触发就 panic。 为每类事件定义专用 handler 类型:
type UserEventHandler func(*UserCreatedEvent)
注册表按事件类型分桶:
map[string][]UserEventHandler
,而不是全塞进一个大 map 发布时强制类型匹配,编译期就能发现 handler 和事件不搭 测试事件流时,别 mock 全局状态,用
chan
捕获触发信号 单元测试里验证「某事件发出后,A 和 B handler 是否被调用」,如果依赖真实注册表,容易因测试顺序污染状态。更干净的做法是把 handler 改成向 channel 发信号,测试只收 channel 数据。 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数组安全、编译 下载 立即学习 “ go语言免费学习笔记(深入) ”; 写测试用的临时 handler:
done := make(chan struct{}, 2); handler := func(e *UserCreatedEvent) { done
注册后触发事件,用
select { case 验证
这样不碰全局 map,也不依赖 sleep,稳定且快 事情说清了就结束。真正麻烦的不是注册和触发,而是 handler 里混了数据库操作、HTTP 调用、又没 context 控制,一出问题整条事件链就卡死——这点比模式本身更值得盯紧。

相关文章