domain/包只允许放聚合根结构体、值对象和repository接口;禁止含基础设施依赖、持久化方法、协议逻辑或生成代码,否则破坏领域纯粹性。

domain/ 包里只能放什么,放错就破防
Go 里 domain/ 不是“放领域代码的地方”,而是“只允许放三类东西”的白名单目录:聚合根结构体(如 *Order)、值对象(如 Money、SKU)、repository 接口(如 OrderRepository)。其他一律禁止。
常见破防点:
-
import "database/sql"或import "net/http"出现在 domain/ 任意文件里 - 在
user.go里写func (u *User) Save() error—— 持久化不是领域职责 - 把 JWT 解析逻辑塞进
domain/auth.go,结果被迫引入crypto/rsa - 用
go:generate在 domain/ 里生成 SQL 扫描代码 —— 生成器输出必须进internal/gen/,不能污染 domain
repository 接口必须定义在 domain/,但实现绝不能在这儿
接口声明位置决定协作契约归属:OrderRepository 接口必须在 domain/order/repository.go,否则 application 层无法面向抽象编程。而它的实现,比如 mysql.OrderRepository,必须放在 infrastructure/mysql/order_repo.go,且可自由 import github.com/go-sql-driver/mysql。
关键约束:
- application 层可以同时持有
*Order和OrderRepository,完成“校验 → 创建 → 存储”闭环 - infra 层 import domain 是合法的;反过来,domain import infra 就直接失守
- 别定义
FindAllByStatusAndDateRange()这种带业务语义的方法 —— 查询组合是 application 层的事,domain 只提供原子能力
聚合根状态变更必须返回 error,且事件不能异步发
聚合根方法(如 Confirm()、Cancel())不是 void 函数,也不该返回 bool。它要表达“这次变更是否被领域规则拒绝”,所以必须返回 error。同时,所有领域事件必须同步暂存到切片中,由 application 层在事务提交后统一发布。
典型错误写法:
func (o *Order) Cancel() {
o.status = StatusCancelled
go func() { eventbus.Publish(OrderCancelled{ID: o.ID}) }() // ❌ panic 丢失、事务不一致、测试不可控
}
正确做法是:
-
func (o *Order) Cancel() []DomainEvent—— 返回事件切片 - application 层拿到事件后,在 DB transaction commit 成功后调用
eventbus.Publish(events) - infra 的 eventbus 实现不能 import domain 的具体 event 类型,要用
interface{}或统一pubsub.Event封装
网关和协议层别硬套 DDD 分层
网关(gateway/)和网络协议层(如 MQTT、gRPC transport)不是 DDD 的适用场景。它们不维护业务状态、不定义聚合根、不发布领域事件,核心是协议转换与流量治理。强行建 domain/、application/ 目录只会让路由匹配、限流策略变成抽象灾难。
更合理的组织方式:
-
pkg/router:纯路径匹配,输入*http.Request,输出serviceID和params -
pkg/middleware:每个中间件只做一件事,如AuthMiddleware只校验 token,不碰日志或响应体 -
transport/mqtt/:放PacketDecoder,只输出*domain.SessionStartEvent,绝不让 domain 类型实现UnmarshalBinary()
真正容易被忽略的是:跨模块依赖必须显式组装。比如 router 和 upstream 之间不能互相 import,得靠 cmd/gateway/main.go 统一注入 —— 否则边界就塌了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











