Go无DDD框架,需靠包边界、接口隔离与显式依赖注入落实DDD;domain/仅含聚合根、值对象、repository接口,禁止infra依赖,ID公开、状态私有,构造函数校验ID,状态变更方法返回error,事件同步暂存、事务提交后发布。

Go 里没有 DDD 框架,也不存在“开箱即用”的领域驱动设计支持——你得靠包结构、接口定义和依赖注入方式自己把边界立住,否则很快就会变成“DDD 名义下的大泥球”。
怎么组织 domain/ 包才不算违规
domain/ 必须只含三类东西:聚合根结构体(如 *Order)、值对象(如 Money、SKU)、repository 接口(如 OrderRepository)。不能出现 database/sql、http、log 或任何 infra 层的 import。
- 聚合根 ID 字段公开(
id string),内部状态字段小写(items []OrderItem),仅通过方法暴露行为 - 构造函数必须返回指针并做基础校验:
if id == "" { return nil, errors.New("id required") } - 所有状态变更方法(如
Cancel())返回error,不静默失败;不返回void或bool - 禁止在 domain/ 里写
func (o *Order) Save() error—— 持久化是 infra 层的事,domain 只管“能不能变”,不管“存哪”
repository 接口该放哪、怎么定义
接口声明在 domain/,实现放在 infrastructure/repository/。这是 Go 实践 DDD 最容易翻车的一环:很多人把接口和实现塞进同一个包,或者让 domain 直接 import infra 的 struct。
- 接口要窄而具体,比如
OrderRepository只需要Save(ctx context.Context, o *Order) error和FindByID(ctx context.Context, id string) (*Order, error),别搞一堆泛型方法 - 不要定义
FindAllByStatusAndDateRange(...)这种带业务语义的查询方法——那是 application 层组装条件的事,domain 层只提供原子能力 - infra 层实现时,用
ent或sqlc生成扫描逻辑,但 domain 层绝不 import 它们的类型;Scan/Value 方法生成到internal/gen/,不进domain/
聚合根事件怎么发才不会丢
别用 goroutine + channel 异步发事件。事务未提交就广播,下游查不到上下文;handler panic,事件直接消失——这不是最终一致性,是随机丢失。
立即学习“go语言免费学习笔记(深入)”;
- 聚合根内部用切片暂存事件:
events []DomainEvent,所有状态变更方法(如Confirm())返回[]DomainEvent - application 层调用聚合方法后,拿到事件切片,在事务成功提交后,再显式调用
eventbus.Publish(events) - infra 层的 eventbus 实现不能 import domain 的具体 event 类型;用
interface{}或统一pubsub.Event封装,避免循环依赖 - 测试时可传入空实现的
eventbus.NoopBus,确保事件逻辑不干扰单元测试隔离性
为什么不用抽象类、也不该套 Java 风格的 Service 接口
Go 没有 abstract class,硬加 type OrderService interface 并在每个方法里塞空实现,只会让 IDE 跳转失灵、mock 成本飙升、业务逻辑被拆得七零八落。
- 状态流转逻辑直接写在聚合根方法里,比如
func (o *Order) TransitionToPaid(payClient PaymentClient) error,依赖显式传入,不藏在 receiver 里 -
PaymentClient是窄接口(只含Charge(ctx, id, amount)),不是大而全的 “支付服务门面” - 跨聚合协作(如订单完成触发库存扣减)由 application 层协调,domain 层不引入其他聚合的引用
- 生成代码只用于 DTO 转换、数据库扫描、基础校验——
Order.IsValid()里含“满减与优惠券互斥”这种规则,必须手写、可读、可 debug
最常被忽略的点是:领域模型的“身份”不是靠继承或框架赋予的,而是靠包路径约束 + 接口签名 + 显式依赖传递共同守住的。一旦某个 domain/ 文件里出现了 import "github.com/xxx/infrastructure",DDD 就已经失效了——这时候删掉所有 DDD 标签,反而更诚实。



















