Event Sourcing本质是状态必须且只能通过重放事件序列推导:需显式类型注册避免interface{}泛化、Apply()须幂等无副作用、快照为性能必需并保证与事件事务一致、事件版本号须全局单调递增。

Event Sourcing 本质不是“存日志”,而是“状态由事件重放得出”
很多人一上来就用 append() 往切片里塞事件、再写到文件或数据库,结果发现查状态慢、回滚错乱、并发写冲突——这不是 Event Sourcing,只是带时间戳的 CRUD 日志。真正的核心是:**当前状态必须且只能通过重放全部(或从快照点起)事件序列推导出来**。这意味着你不能直接更新状态字段,也不能跳过某条事件;所有变更入口必须经过事件生成 → 持久化 → 应用三步闭环。
用 interface{} 做事件类型泛化?别这么干
常见错误是定义一个空接口切片 []interface{} 存各种事件,导致反序列化时无法还原具体类型,后续重放时 panic 或静默失败。Go 没有运行时类型反射自动匹配的能力,必须显式处理。
- 用自定义事件接口 + 显式类型注册:定义
type Event interface { EventType() string; Version() uint } - 每个事件结构体实现该接口,并在启动时注册到全局 map:
eventRegistry["OrderCreated"] = func() Event { return &OrderCreated{} } - 持久化时存
EventType字段;加载时先读字段,再用 registry 创建实例,最后json.Unmarshal()到该实例 - 避免用
map[string]interface{}存事件数据——字段名拼错、类型不一致、缺失字段全在运行时才暴露
Apply() 方法必须幂等且无副作用
重放事件时可能多次调用 Apply()(比如从快照恢复后补事件、测试重放、调试重试),如果方法里写了 DB 插入、发 HTTP 请求、改全局变量,就会出问题。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
Apply()只允许修改当前聚合根(Aggregate Root)的内存状态字段,例如a.status = event.Status、a.balance += event.Amount - 禁止在
Apply()中调用db.Exec()、log.Printf()、time.Now()等外部依赖 - 如果需要记录审计信息,应由上层调用方在事件提交成功后统一处理,而非混进
Apply() - 测试时可断言:对同一事件连续调用两次
Apply(),状态值不变
快照(Snapshot)不是可选项,是并发与性能的生死线
当一个订单聚合重放 5000 条事件才能得到当前状态,每次查询都这么干,CPU 和延迟立刻崩盘。快照不是“锦上添花”,而是必须设计的环节。
立即学习“go语言免费学习笔记(深入)”;
- 快照触发条件建议按事件数量(如每 100 条)或时间窗口(如每 24 小时),而非固定时间点
- 快照内容只存聚合根当前状态字段,**不存事件历史、不存引用对象指针、不存未导出字段**(否则 JSON 序列化失败)
- 加载逻辑必须兼容:先查最新快照 → 若存在则初始化聚合 → 再重放该快照之后的所有事件;若无快照,则从头重放
- 快照和事件必须共用同一存储事务边界(例如用 PostgreSQL 的
BEGIN/COMMIT包裹快照写入 + 事件追加),否则出现快照比事件新、导致状态倒退
最常被忽略的一点:事件版本号(Version)必须全局单调递增,且与快照版本对齐。一旦跳号、重复或乱序,重放结果必然不可信——这比数据丢更快地毁掉整个溯源链路。

















