GORM 只能出现在 infrastructure 或 adapter 层,绝不能被 domain 或 usecase 直接 import;domain 必须无技术栈感知,禁止出现 gorm.Model、gorm.DB 等类型,接口定义在 domain/usecase,实现封装在 infrastructure/gorm/ 中,通过 mock 验证隔离性。

在 Clean Architecture 中,GORM 只能出现在最外层(infrastructure 或 adapter),绝不能被 domain 或 usecase 直接 import —— 这是分层是否有效的硬性边界,不是风格问题,而是依赖方向是否合规的判断依据。
为什么 GORM 不能出现在 domain/usecase 包里
domain 层必须完全无技术栈感知:不能出现 gorm.Model、gorm.DB、gorm.Tag,也不能用 time.Time 或 sql.NullString。一旦你写 type User struct { gorm.Model },就等于把业务实体和 ORM 绑死了;测试时 mock 不了,换数据库时改不动,连 JSON 序列化都可能 panic(sql.NullString 的字段不可导出)。
常见错误现象:
-
undefined: gorm在domain/user.go中报错 —— 实际是编译前就被 Go 的 import 规则拦住了 - 单元测试中调用
userRepo.FindByID()却触发真实数据库连接 —— 因为接口实现和定义混在同一包,无法注入 mock - 想把 PostgreSQL 换成 SQLite,结果发现
usecase里有import "gorm.io/driver/postgres"—— 依赖箭头反了,架构已失效
GORM 应该放在哪?怎么封装才合规
正确位置是 infrastructure/database/ 或 adapter/repository/ 下的子包,比如 infrastructure/gorm/user_repository.go。它负责两件事:实现 domain 定义的接口,并把 GORM 的细节全部吞掉。
实操要点:
- 接口必须定义在
domain或usecase包中,例如type UserRepository interface { FindByID(id string) (*domain.User, error) } - 实现结构体字段只保留
*gorm.DB,不暴露任何 GORM 类型到上层,返回值只能是domain.User或其指针 - 所有 GORM tag(如
gorm:"column:email")只能出现在 infrastructure 层的映射结构体里,domain.User里禁止出现 - 时间字段统一用
*time.Time或自定义Clock接口注入,不直接用CreatedAt time.Time
如何验证 GORM 分层是否真正生效
最直接的办法是检查 import 路径:打开任意 usecase 文件,go list -f '{{.Imports}}' ./usecase 输出里不能含 gorm.io、github.com/go-sql-driver 等任何基础设施包。
另一个关键信号是测试可隔离性:
- 运行
go test ./usecase时不应触发网络或数据库初始化 - 你能轻松用
mockUserRepo := &MockUserRepository{}替换真实实现,且不引入任何 GORM 依赖 -
domain包go build成功,且go list -f '{{.Deps}}' ./domain返回为空或仅标准库
真正难的不是写对目录,而是守住那条 import 线——跨过去一次,整层就塌了。很多人重构到一半发现 usecase 里悄悄 import 了 GORM,不是忘了删,而是当初为了“快一点”绕开了接口抽象。那点省下的十分钟,后期会变成三天调试时间。


















