go 中应通过独立接口分离 command 和 query handler:command handler 接收 context 和命令,仅变更状态并返回 error;query handler 接收 context 和查询,只读取并返回 dto;仓储接口必须拆分为 userwriterrepo 和 userreaderrepo,禁止混用。

Go 里怎么拆分 Command 和 Query 的 handler?
直接用两个独立的接口类型,别塞进一个 struct 里。CQRS 的核心是职责分离,不是“多写几个方法”。
常见错误是定义 type Service struct{},然后在里面塞 HandleCreateUser() 和 GetUserByID() —— 这只是 CRUD 换了个名字,没真正分离读写模型。
- Command handler 接收
context.Context和命令结构体(如*CreateUserCmd),只做状态变更,返回 error;不返回业务数据 - Query handler 接收
context.Context和查询结构体(如*GetUserQuery),只读 DB 或 cache,返回 DTO(如UserView),绝不返回 domain entity - 两者依赖的仓储接口必须分开:
UserWriterRepo和UserReaderRepo,实现可以共用同一张表,但接口不能互相调用
为什么不能共用同一个数据库事务来包装 Command + Query?
因为 CQRS 不是“把读写逻辑分开写”,而是“允许读写走不同路径”。一旦你在 command handler 里顺手调个 query 方法查刚写入的数据,就等于在事务内做读,既破坏了查询侧的缓存友好性,又拖慢了写路径。
典型翻车场景:用户注册后立刻调 GetUserByID() 返回结果,结果发现查不到——因为写还没 commit,或者你用了最终一致性的 event sourcing。
- command handler 内禁止调用任何 query handler 或 reader repo
- 需要“写后即读”的场景,要么显式
tx.Commit()后再查,要么改用SELECT ... FOR UPDATE(仅限强一致性需求) - 更稳妥的做法是让前端轮询或监听事件,query side 自己更新视图表
Event Sourcing 在 Go 里怎么避免重放时 panic?
重放事件时最常遇到的是:老事件结构体字段缺失、新 handler 对 nil 字段 panic、或时间戳乱序导致状态错乱。
Go 没有运行时 schema 检查,所以得靠编码约束和测试兜底,不是靠框架自动修复。
- 所有事件 struct 必须用
json.RawMessage或自定义 unmarshal 方法处理可选字段,别直接绑定到非指针字段 - 重放前先跑
go test -run TestReplayOldEvents,用真实历史 event JSON 测试每个 handler 是否 panic - 每个 event type 必须带
Version int字段,handler 内用 switch 区分版本逻辑,旧版本字段缺失时提供默认值 - 别在 handler 里做耗时操作(比如发邮件),event sourcing 的重放可能批量触发几十万次
要不要用第三方库(比如 go-cqrs、cqrs-go)?
不推荐。这些库大多抽象过度,强行统一 message bus、serializer、storage,反而让你更难控制 error handling、context 传递和 trace 注入。
Go 的优势是小而明确:用 func(context.Context, *Cmd) error 就是 command handler,用 func(context.Context, *Query) (*DTO, error) 就是 query handler。加一层 interface 就够了。
- bus 用
github.com/ThreeDotsLabs/watermill或原生chan+sync.WaitGroup足够 - event store 直接写 PostgreSQL 的
events表,每行存stream_id、type、data jsonb、version - 唯一值得引入的工具是
gofrs/uuid生成 stream ID,别用math/rand
复杂点不在模式本身,而在你怎么隔离 error 边界、怎么让 query side 的数据更新延迟可测、以及怎么让运维能快速定位某条 command 卡在哪一环。这些没法靠库解决。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











