Golang 框架中实现领域驱动设计(DDD)的实践

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

golang 框架中实现领域驱动设计(ddd)的实践

Go 里没有 DDD 框架,所谓“Golang 框架中实现 DDD”是个误导性说法——你不会在 go get 列表里找到 github.com/ddd-go/corego-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 实现该接口,可 import entsqlc 生成的类型,但 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

Golang实现基于LRU淘汰策略的高并发字符串键值缓存系统
上一篇 2026-07-01 13:39
如何通过CSS中的print媒体查询优化网页打印时的布局显示?
下一篇 2026-07-01 13:39

相关推荐