watermill不是开箱即用事件总线,必须通过message.newrouter初始化并用router.addhandler注册handler,否则将导致goroutine泄漏、panic中断、中间件失效;handler需严格返回nil以自动ack,启用manualack时须显式调msg.ack()。

Watermill 不是“引入就能用”的事件总线,必须走 Router 初始化流程
跳过 message.NewRouter 直接用 kafka.NewPublisher 或手写 for range 消费,上线后必然出问题:goroutine 泄漏、panic 全局中断、中间件失效。Router 是 Watermill 的调度中枢,所有 handler 生命周期、错误隔离、中间件注入都依赖它。
正确初始化姿势:
- 调用
message.NewRouter创建实例,传入 logger 和配置(如router.WithMiddleware) - 每个 topic 处理逻辑必须通过
router.AddHandler注册,不能靠反射或自动发现 - 启动前调
router.Run,它会自动管理 goroutine、recover panic、等待所有 handler 就绪
Handler 函数签名和返回值决定消息是否 Ack,不是“执行完就提交”
Watermill 把消费成功与否完全交给 handler 返回值,底层驱动(Kafka/RabbitMQ)只负责投递和 offset 提交时机。常见错误是 handler 里写了业务逻辑但忘了 return nil,导致消息无限重试。
必须严格遵循签名:func(ctx context.Context, msg *message.Message) error
- 返回
nil→ 自动调msg.Ack()(前提是未启用 ManualAck) - 返回非
nil error或 panic → 触发重试(默认 3 次),不 Ack - 若启用了
ManualAck模式,必须显式调msg.Ack(),否则 offset 永远卡住 - 业务逻辑务必用
ctx, cancel := context.WithTimeout(msg.Context(), 5*time.Second)包裹,超时值要小于 Kafka 的session.timeout.ms
Kafka 场景下 ConsumerGroup 和 Topic 声明缺一不可
没设 group.id 或没提前声明 topic/queue,会导致重复消费或启动失败。Watermill 不自动建资源,它只消费已存在的 topic/queue。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
关键配置点:
- Kafka subscriber 必须传
consumerGroup: "your-group-id",否则每个实例都被 Kafka 当作独立消费者 - topic 必须在 Kafka 集群中已存在,且分区数、replication factor 符合预期;Watermill 不做创建或变更
- RabbitMQ subscriber 必须指定
queue名,且该 queue 需预声明(durable=true, autoDelete=false) - Publisher 端要配
publisher.Confirm(true)(Kafka)或amqp.Publishing.DeliveryMode = amqp.Persistent(RabbitMQ),否则 broker 重启消息全丢
领域事件发布必须隔离 infrastructure,domain 层只定义 Publisher 接口
把 kafka.Producer.SendMessage 写进 domain 层,等于让业务逻辑强耦合消息中间件,单元测试无法 mock,分层彻底失效。
正确分层方式:
- domain 包里只定义事件 struct(如
OrderPaidEvent)和Publisher接口(type Publisher interface { Publish(Event) }) - application 层实现该接口,注入具体 publisher(
KafkaPublisher或InMemoryPublisher) - 测试时用内存实现,断言
events[0].ID == expectedID即可,无需启动 Kafka - 跨服务消费必须自己做幂等:用
event.ID(ULID/UUIDv7)为 key,在 Redis 或 DB 中记录是否已处理,别用order_id这类业务字段当幂等键
真正难的不是写 handler,而是把事件契约(字段名、类型、是否允许 null、版本号)在 domain 层定死——一旦上线,改一个字段都可能引发下游服务 panic。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










