Go中DomainEvent应隔离实现细节,由Application Service在事务提交后发布;内存EventBus仅适用于非关键场景,生产环境需用outbox模式+消息队列;Handler须幂等、无状态。

Go 本身没有内置的事件总线或领域事件抽象,直接套用 DDD 的 DomainEvent 模式容易写出难以测试、耦合严重、生命周期混乱的代码。关键不是“怎么发事件”,而是“谁负责注册监听、谁保证顺序、谁处理失败、事件是否要持久化”——这些决定了你该用轻量通知,还是引入消息队列。
DomainEvent 接口设计必须隔离实现细节
别让领域对象直接依赖 eventbus.Publish 或 kafka.Producer。定义纯内存、无副作用的接口:
type DomainEvent interface {
EventName() string
AggregateID() string
Version() uint64
}
所有事件结构体只实现这个接口,不嵌入任何 handler、ctx 或 broker 相关字段。好处是:
- 单元测试时可直接构造事件实例,无需 mock 外部系统
- 后续替换 EventBus 实现(如从内存切到 NATS)时,领域层零修改
- 避免在
UserCreated里偷偷调用sendEmail()这类违反单一职责的操作
事件发布时机:Aggregate 内部不调用 Publish,由 Application Service 控制
常见错误是让 User 聚合根在 Create() 方法末尾直接调用 eventbus.Publish(e)。这会导致:
- 事务边界模糊:DB 提交前发事件,下游可能读到未提交数据
- 测试困难:每次测
User.Create()都得 stub eventbus - 无法批量发布:多个聚合变更时,没法合并为一次事务后统一发事件
正确做法是 Aggregate 只记录事件:
func (u *User) Create(name string) {
u.name = name
u.events = append(u.events, &UserCreated{ID: u.id, Name: name})
}
func (u *User) PopEvents() []DomainEvent {
events := u.events
u.events = nil
return events
}
Application Service 在事务提交后显式收集并发布:
tx := db.Begin()
user.Create("alice")
// ... 其他操作
if err := tx.Commit(); err != nil {
return err
}
for _, e := range user.PopEvents() {
eventbus.Publish(e) // 此时才真正发出
}
内存 EventBus 仅适用于单进程、非关键业务场景
如果你用 github.com/asaskevich/EventBus 或手写 map+chan 实现 EventBus,它只适合:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- CLI 工具、本地开发服务、测试环境
- 事件不需持久化、丢失可接受(如 UI 状态刷新)
- 无跨服务通信需求
问题在于:
- goroutine 泄漏风险:监听器 panic 后,未 recover 的 handler 会 kill 整个 bus
- 无重试机制:网络抖动或下游宕机时事件永久丢失
- 无法保证顺序:多个 listener 并发执行,
UserCreated和UserVerified可能乱序
生产环境建议把事件落地为 DB 表(outbox pattern),再由独立 worker 读取投递到 Kafka/RabbitMQ。
Event Handler 必须幂等且无状态
Handler 不该持有缓存、连接池或全局变量。例如下面这种写法是危险的:
var client *http.Client // 全局单例,可能被并发调用污染
func HandleUserCreated(e *UserCreated) {
client.Post(...) // 并发下可能复用已关闭的连接
}
正确方式是每次 handler 执行都新建必要依赖,或通过依赖注入传入线程安全实例:
type UserCreatedHandler struct {
emailSender EmailSender // interface,由 DI 容器注入
logger *log.Logger
}
func (h *UserCreatedHandler) Handle(e *UserCreated) error {
return h.emailSender.Send(e.ID, "Welcome!")
}
同时必须在 handler 内部做幂等判断(如查 DB 是否已发过欢迎邮件),不能指望上游重试机制来兜底。
最常被忽略的一点:领域事件不是日志,也不是审计记录。如果一个事件只被一个下游消费,且语义上属于“通知”而非“契约”,那它大概率不该进领域模型——直接用函数回调或 channel 更轻量。强行塞进 DomainEvent 体系,只会增加理解成本和维护负担。


















