Go里没有DDD框架,所谓“Golang框架中实现DDD”是误导;domain包禁止import database/sql或gin,状态变更须走封装方法而非直接赋值,repository接口定义在domain层、实现置于infrastructure层,事件须同步返回切片而非goroutine异步发布。

Go 里没有 DDD 框架,所谓“Golang 框架中实现 DDD”是个误导性说法——你不会在 go get 列表里找到 github.com/ddd-go/core 或 go-ddd 这类包,强行引入只会让项目变重、测试失灵、IDE 跳转失效。
domain 包里为什么不能 import database/sql 或 gin
领域模型(如 *Order、*Customer)一旦依赖 database/sql 或 HTTP 框架,就等于把存储细节和传输协议泄漏进业务核心。后果很直接:
- 单元测试跑不起来:一执行
TestOrder_Confirm就 panic,因为缺 DB 连接或 context - 重构成本飙升:想把 MySQL 换成 SQLite?得改遍所有
*sql.DB依赖的 domain 文件 - ID E 失去语义:Ctrl+Click
order.Save()跳到的不是业务逻辑,而是某个 infra 包里的 SQL 拼接
正确做法是让 Order 只关心“已取消订单不能发货”,校验逻辑写在 order.Ship() 里;而“存哪、怎么存”,交给 OrderRepository 接口,实现放在 infrastructure/mysql/order_repo.go。
聚合根状态变更必须走方法,不能直接赋值
这是 Go 实现聚合根最易被忽略的一点:所有状态变更必须封装在小写方法内,禁止外部直接写字段。否则无法保证不变量(比如“已发货的订单不能取消”)。
立即学习“go语言免费学习笔记(深入)”;
- 常见错误:把
Status设为导出字段(Status string),导致任意包都能赋值 - 常见错误:在 handler 或 service 里直接写
order.Status = "shipped",绕过领域逻辑 - 常见错误:把库存校验逻辑放在 API 层,而不是
(*Order).Ship(inventory InventoryService)内部
正确写法示例:
func (o *Order) Ship(inventory InventoryService) error {
if o.Status != "confirmed" {
return errors.New("order must be confirmed before shipping")
}
if !inventory.HasStock(o.Items) {
return errors.New("insufficient inventory")
}
o.Status = "shipped"
o.ShippedAt = time.Now()
return nil
}
repository 接口必须定义在 domain/,实现放在 infrastructure/
接口声明位置决定分层是否成立。很多人把接口和实现塞进同一个包,或者让 domain/ 直接 import infra 的 struct,结果分层瞬间塌方。
-
domain/order/repository.go中只定义窄接口:type OrderRepository interface { Save(ctx context.Context, o *Order) error; FindByID(ctx context.Context, id string) (*Order, error) } -
infrastructure/mysql/order_repo.go实现该接口,可 importent或sqlc生成的类型,但domain/绝对不能 import 它们 - 别定义
FindAllByStatusAndDateRange()这种带业务语义的查询方法——那是 application 层组装条件的事,domain 层只提供原子能力
聚合根事件不能用 goroutine 异步发,必须同步返回切片
常见错误写法:
func (o *Order) Cancel() {
o.status = StatusCancelled
go func() {
eventbus.Publish(OrderCancelled{ID: o.ID}) // ❌ 危险!
}()
}
问题有三个:
- panic 丢失:goroutine 里 publish 失败,主流程完全不知情
- 事务不一致:DB 事务还没 commit,下游 handler 就去查订单,查不到
- 测试不可控:你没法断言“这次 Cancel 是否发了事件”,因为它是异步的、无返回值的
正确做法是聚合根内部用切片暂存事件:events []DomainEvent,所有变更方法(如 Confirm())返回 []DomainEvent;application 层拿到后,在事务提交成功后再显式调用 eventbus.Publish(events)。
真正难的不是写接口或分目录,而是每次写代码时都得问一句:这个 import 是不是越界了?这个字段是不是不该导出?这个 error 是不是该由领域方法返回而不是静默吞掉?这些纪律没法靠框架 enforce,只能靠人盯住每一行。
文章来自机圈观察员网,发布者:,转载请注明出处:https://www.jqgcy.com/shoujipingce/123782.html