go中落地ddd不依赖框架,核心是严守分层边界:domain层只定义接口(如userrepository),实现放infra层;限界上下文宜用独立go.mod隔离;工厂函数仅负责对象构建与校验,不涉外部调用。

Go 里没有 DDD 框架,别找 ddd.NewDomainModel()
Go 语言本身不提供 DDD 原语,也没有被广泛采用的“DDD 框架”。你不会在 go get 列表里看到 github.com/ddd-go/core 这类包——真有,大概率是玩具项目或过度抽象的模板。所谓落地,不是引入一个库,而是用 Go 原生机制守住几条线:package 边界、interface 定义位置、依赖注入方式。一旦你 import 了 gorm 或 gin 到 domain/ 目录下,DDD 就已经失守。
UserRepository 接口必须定义在 domain/,实现必须在 infra/mysql/
这是 DDD 在 Go 中最易被违反也最关键的约定。领域层(domain/)只负责表达“要存什么”和“要查什么”,不关心“怎么存”。比如:
type UserRepository interface {
Save(ctx context.Context, u *User) error
GetByID(ctx context.Context, id UserID) (*User, error)
}
这个接口必须放在 domain/ 下;而它的实现,比如 mysql.UserRepository,必须放在 infra/mysql/ 或类似路径下,并且该实现可以自由 import database/sql、github.com/go-sql-driver/mysql 等。常见错误包括:
- 把
Save()方法直接写进User结构体里,变成u.Save() - 在
domain/user.go里 import"github.com/jmoiron/sqlx" - 让
application层直接 new 一个mysql.UserRepository{db: db},绕过依赖注入
聚合根工厂函数比构造器更安全,但别让它承担校验以外的逻辑
用工厂函数(如 NewCustomer(name string) (Customer, error))替代裸 Customer{...} 初始化,能集中处理 ID 生成、必填字段校验、默认值填充等。但它不该做以下事:
- 调用
repository.Save()—— 这属于应用层职责 - 触发外部 HTTP 请求或发消息 —— 这属于领域事件发布,应由应用层协调
- 访问配置或环境变量 —— 工厂只管“对象是否合法”,不管“系统怎么配”
工厂返回的是纯内存对象,不含任何运行时依赖。它可被单元测试完全隔离,不需要 mock 数据库或网络。
限界上下文 ≠ 目录名,但 Go 项目里它最好就是一个独立 go.mod
一个真实的限界上下文(比如 order)在 Go 中最稳健的落地方式,是把它做成一个子模块:目录下有自己独立的 go.mod,比如 order/go.mod。这样能物理隔离依赖,避免 user 包意外 import order 的 infra 实现。实际中容易忽略的是:
- 跨上下文调用必须走明确契约,比如通过
order.OrderPlacedEvent发布事件,而不是user.OrderService.Create() -
domain/order/和domain/user/不该共享同一个go.mod,否则无法阻止循环引用 - API 路由层(如
server/handler/order.go)只能依赖application/order,不能越过应用层直连domain/order
边界模糊的地方,往往最先出问题:比如订单创建时需要扣减库存,到底是让 order 上下文调用 inventory 的 API,还是发事件由 inventory 自行响应?这得由业务一致性要求决定,不是技术能拍板的。











