如何使用Golang实现观察者模式_Golang观察者模式事件通知实现

发布时间 - 2026-02-01 00:00:00    点击率:
Go可用接口+map+互斥锁轻量实现观察者模式,Observer定义Update方法,Subject用map[string]Observer管理并支持Attach/Detach,Notify异步分发事件,需结合context或规范解绑防泄漏。

Go 语言没有内置的 Observer 接口或事件总线,但可以用接口 + 切片 + 方法绑定的方式轻量实现观察者模式,关键在于避免循环引用和确保通知时的并发安全。

Observer 接口和 Subject 结构体定义核心契约

观察者必须实现统一的通知方法,被观察者(Subject)则维护观察者列表并提供注册/移除/通知能力。不要用泛型约束观察者类型,否则会限制使用场景;用接口更灵活。

  • Observer 接口只定义一个 Update(event interface{}) 方法,接受任意事件数据
  • Subject 是普通结构体,字段包含 observers []Observer 和互斥锁 mu sync.RWMutex
  • 注册方法 Attach(o Observer) 需加写锁,避免并发 append 导致 panic
  • 通知方法 Notify(event interface{}) 用读锁遍历,但注意:不能在遍历时修改 observers 切片

避免在 Notify 中同步调用观察者导致阻塞或死锁

如果某个 Observer.Update() 执行耗时或调用了 Detach(),同步遍历会卡住整个通知流,还可能引发递归修改切片的 panic。生产环境必须异步化。

  • Notify 内启动 goroutine 分发事件,但需控制并发数,避免 goroutine 泛滥
  • 更稳妥的做法是把事件推入 channel,由单独的 dispatcher goroutine 消费
  • 若观察者数量少且逻辑简单(如日志、指标上报),可保留同步调用,但要明确标注“调用方需保证 Update 快速返回”

观察者注册时如何防止重复添加和内存泄漏

Go 没有对象身份比较(如 Java 的 ==),直接用 == 比较接口值不可靠。重复注册会导致同一观察者收到多次通知;不清理已失效的观察者(如 HTTP handler 关闭后未解绑)会造成内存泄漏。

  • 不依赖地址比较,改用带唯

    一 ID 的观察者(例如 type LoggerObserver struct { id string }),Attach 前先遍历检查 id
  • 提供 Detach(id string) 显式解绑,比传入接口值更可控
  • 对长生命周期的观察者(如全局 metrics collector),建议配合 context.Context 实现自动注销:在 Update 中检测 ctx.Err() != nil 后主动调用 Detach
type Observer interface {
    Update(event interface{})
}

type Subject struct {
    mu        sync.RWMutex
    observers map[string]Observer // 改用 map 便于按 id 查找/删除
}

func (s *Subject) Attach(id string, o Observer) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.observers[id] = o
}

func (s *Subject) Detach(id string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    delete(s.observers, id)
}

func (s *Subject) Notify(event interface{}) {
    s.mu.RLock()
    obs := make([]Observer, 0, len(s.observers))
    for _, o := range s.observers {
        obs = append(obs, o)
    }
    s.mu.RUnlock()

    for _, o := range obs {
        go o.Update(event) // 异步分发
    }
}

真正难处理的是跨 goroutine 生命周期管理——比如一个 HTTP handler 注册为观察者,但 handler 返回后其闭包变量仍被 subject 持有。这时候光靠 Detach 不够,得结合 context 或 weak reference 思路(如用 sync.Map 存储带弱引用标记的观察者),但 Go 标准库不支持真正的弱引用,所以最实际的做法还是靠代码规范:谁注册,谁负责在合适时机解绑。


# java  # go  # golang  # app  # 代码规范  # 标准库  # String  # 结构体  # 递归  # 循环  # 接口  # Struct  # Interface  # Event  # 泛型  # 闭包  # 切片  # nil  # append  # map  # 并发  # channel  # 对象  # 事件  # 异步  # http  # 遍历  # 死锁  # 的是  # 互斥  # 可以用  # 能在  # 不支持  # 但要  # 则会 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 简历没回改:利用AI润色让你的文字更专业  Windows11怎样设置电源计划_Windows11电源计划调整攻略【指南】  百度输入法ai组件怎么删除 百度输入法ai组件移除工具  Laravel怎么实现微信登录_Laravel Socialite第三方登录集成  PHP怎么接收前端传的文件路径_处理文件路径参数接收方法【汇总】  PHP正则匹配日期和时间(时间戳转换)的实例代码  如何用VPS主机快速搭建个人网站?  如何挑选高效建站主机与优质域名?  如何用狗爹虚拟主机快速搭建网站?  如何自定义建站之星网站的导航菜单样式?  实例解析angularjs的filter过滤器  如何在Windows服务器上快速搭建网站?  北京网页设计制作网站有哪些,继续教育自动播放怎么设置?  电商网站制作多少钱一个,电子商务公司的网站制作费用计入什么科目?  Laravel观察者模式如何使用_Laravel Model Observer配置  Laravel怎么实现观察者模式Observer_Laravel模型事件监听与解耦开发【指南】  Python面向对象测试方法_mock解析【教程】  Firefox Developer Edition开发者版本入口  如何在HTML表单中获取用户输入并用JavaScript动态控制复利计算循环  如何快速搭建高效WAP手机网站吸引移动用户?  长沙企业网站制作哪家好,长沙水业集团官方网站?  详解免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)  如何快速生成专业多端适配建站电话?  Android自定义控件实现温度旋转按钮效果  JavaScript数据类型有哪些_如何准确判断一个变量的类型  谷歌浏览器下载文件时中断怎么办 Google Chrome下载管理修复  详解Huffman编码算法之Java实现  Laravel如何与Docker(Sail)协同开发?(环境搭建教程)  如何在IIS中新建站点并解决端口绑定冲突?  Laravel如何优化应用性能?(缓存和优化命令)  如何在阿里云虚拟主机上快速搭建个人网站?  今日头条微视频如何找选题 今日头条微视频找选题技巧【指南】  Laravel如何实现密码重置功能_Laravel密码找回与重置流程  如何在建站宝盒中设置产品搜索功能?  电视网站制作tvbox接口,云海电视怎样自定义添加电视源?  JavaScript如何实现音频处理_Web Audio API如何工作?  如何在万网ECS上快速搭建专属网站?  如何使用 Go 正则表达式精准提取括号内首个纯字母标识符(忽略数字与嵌套)  黑客如何通过漏洞一步步攻陷网站服务器?  Laravel模型事件有哪些_Laravel Model Event生命周期详解  php json中文编码为null的解决办法  Laravel怎么实现一对多关联查询_Laravel Eloquent模型关系定义与预加载【实战】  高性价比服务器租赁——企业级配置与24小时运维服务  微信推文制作网站有哪些,怎么做微信推文,急?  如何在橙子建站中快速调整背景颜色?  Laravel如何使用Service Container和依赖注入?(代码示例)  Laravel如何使用Facades(门面)及其工作原理_Laravel门面模式与底层机制  什么是javascript作用域_全局和局部作用域有什么区别?  浅谈Javascript中的Label语句  制作网站软件推荐手机版,如何制作属于自己的手机网站app应用?