domain/包只允许放聚合根(如*order)、值对象(如money)、repository接口(如orderrepository);禁止import infra层代码,id公开、状态私有,构造函数校验id,状态变更方法返回error,事件同步暂存、事务提交后发布。

domain/ 包不能 import 任何 infra 层代码,否则 DDD 边界就塌了——这是 Go 微服务里最常被忽略、也最容易导致后期重构灾难的点。
domain/ 包里只允许放什么?
Go 没有 DDD 框架,所有边界都靠包结构和 import 约束来维持。一旦 domain/ 出现 database/sql、log、http 或具体 ORM 类型(如 ent.Client),你就已经掉进“名义 DDD”陷阱。
- 只放三类东西:
*Order(聚合根)、Money(值对象)、OrderRepository(接口) - ID 字段必须公开(
id string),状态字段全部小写(items []OrderItem) - 构造函数返回
*Order并校验 ID:if id == "" { return nil, errors.New("id required") } - 所有变更方法(
Confirm()、Cancel())必须返回error,不接受 void 或 bool
OrderRepository 接口该定义在哪儿?怎么写才不越界?
接口声明在 domain/,实现放在 infrastructure/repository/——这是 Go 实践 DDD 最高频翻车点。很多人把接口和实现塞进同一个包,或者让 domain/ 直接 import infrastructure/ 的 struct。
- 接口要窄:
Save(ctx context.Context, o *Order) error和FindByID(ctx context.Context, id string) (*Order, error)就够了 - 禁止定义
FindAllByStatusAndDateRange()这种带业务语义的方法——那是application/层组装查询条件的事 - infra 层实现时,用
ent或sqlc生成扫描逻辑,但domain/绝对不能 import 它们的类型;Scan/Value方法生成到internal/gen/,不进domain/
聚合根里的事件怎么发才不会丢?
别用 go func() {}() 异步发事件。事务没提交就广播,下游查不到数据;handler panic,事件直接消失——这不是最终一致性,是随机丢失。
- 聚合根内部用切片暂存:
events []DomainEvent - 每个状态变更方法(如
Confirm())返回新事件切片,不触发发布 - application 层调用后,在事务成功提交后,再显式调用
eventbus.Publish(events) - infra 层的
eventbus实现不能 importdomain的具体 event 类型;用interface{}或统一pubsub.Event封装,避免循环依赖
限界上下文怎么拆才不变成分布式单体?
拆分不是按技术层,而是看业务归属。订单和订单明细必须在同一上下文;订单和用户可以拆,但前提是它们的数据存储、变更频率、维护团队互不重叠。
- 数据归属是否独立:共享核心表 → 不拆
- 变更频率是否差异大:促销规则天天改,用户认证半年不动 → 可拆
- 团队协作是否冲突:两个模块由不同团队维护,且频繁 git merge 冲突 → 拆
- 目录结构上,每个上下文对应一个
internal/order/、internal/user/,各自含domain/、application/、infrastructure/
*Order,而是让所有开发者在每次 import 时都下意识停顿半秒:这个包,它到底属于哪一层?golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











