
Go 里没有抽象类、没有“DDD 框架”、也不需要照搬 Java 的分层接口模板——硬套只会让代码变重、测试失灵、IDE 跳转失效。DDD 在 Go 中能落地,靠的是包路径约束、窄接口、显式依赖和结构体组合,不是靠生成器或泛型包装。
domain 层为什么不能 import database/sql 或 gin
领域模型(如 Order、Customer)一旦引入 database/sql 或 HTTP 框架,就等于把存储细节和传输协议泄漏进业务核心。这会导致:
无法离线单元测试:一跑 TestOrder_ApplyDiscount 就 panic,因为缺 DB 连接重构成本飙升:想把 MySQL 换成 SQLite?得改遍所有 *sql.DB 依赖的 domain 文件IDE 失去语义:Ctrl+Click Order.Save() 跳到的不是业务逻辑,而是某个 infra 包里的 SQL 拼接
正确做法是让 Order 只关心“满减不能和优惠券同用”,校验逻辑写在 Order.Validate() 里;而“存到哪、怎么存”,交给 OrderRepository 接口,实现放在 infrastructure/mysql/order_repo.go。
Repository 接口该定义在 domain 还是 infra
必须定义在 domain/ 下,比如 domain/order/repository.go:
立即学习“go语言免费学习笔记(深入)”;
type OrderRepository interface { Save(ctx context.Context, o *Order) error FindByID(ctx context.Context, id OrderID) (*Order, error)}
原因很直接:
领域层要表达“我需要能存、能查”,这是它的协作契约,不是 infra 的实现细节application 层(如 application/place_order.go)才能同时持有 *Order 和 OrderRepository,完成“校验 → 创建 → 存储”闭环infra 层实现时 import domain 包是合法的;反过来,domain import infra 就彻底破防
别写 mysql.OrderRepository 这种类型——它会诱使你在 domain 里直接 new 它,绕过接口抽象。
聚合根事件为什么不能用 channel 异步发布
常见错误是这样写 order.Cancel():
func (o *Order) Cancel() { o.status = StatusCancelled go func() { eventbus.Publish(OrderCancelled{ID: o.ID}) }() // ❌ 危险!}
问题有三个:
panic 丢失:goroutine 里 publish 失败,主流程完全不知情事务不一致:DB 事务还没 commit,下游 event handler 就去查订单,查不到测试不可控:你没法断言“这次 Cancel 是否发了事件”,因为它是异步的、无返回值的
正确模式是同步返回事件切片:
func (o *Order) Cancel() []Event { o.status = StatusCancelled return []Event{OrderCancelled{ID: o.ID}}}
然后由 application 层在事务提交后统一处理:
if err := repo.Save(ctx, order); err != nil { return err}eventbus.Publish(orderEvents…) // ✅ 显式、可测、可控
go:generate 能省什么,又绝不能碰什么
生成代码只该干两件事:数据搬运 + 基础校验。
✅ 推荐生成:Scan()/Value()(用 entgen)、ToDTO()(用 stringer 或自定义模板)、Validate() 入参(配合 go-playground/validator 标签)❌ 绝对禁止生成:Order.IsValid() 里的业务规则(如“预售订单不能取消”)、Apply() 中的状态流转逻辑、任何含 if/else 判断的领域行为
生成代码必须进 internal/gen/,永远不进 domain/ —— 否则 PR 里全是 Scan() 方法字段顺序调整 这类噪音,团队很快就会关掉生成逻辑。

评论(0)