Go Clean Architecture要求domain层禁用time.Time和sql.NullString,因其绑定实现细节、破坏可测试性;须改用string/int64等基础类型或自定义类型,转换逻辑下沉至adapter层。

Go 项目一旦超过 3 个接口、2 个数据库表、4 人协同,不按 Clean Architecture 约束依赖流向,两周内就会出现 internal/service 直接调用 database/sql、pkg/utils 里塞进用户积分逻辑、cmd/main.go 初始化 Redis 客户端却没抽象成接口——这不是“快速迭代”,是给后续所有人挖坑。
为什么 internal/domain 里不能出现 time.Time 或 sql.NullString
domain 层不是“放结构体的地方”,而是“定义业务不变性的边界”。只要它依赖了标准库或驱动类型,这个边界就破了。
-
time.Time带有时区行为和格式化方法,一旦写进User.CreatedAt,你就无法在测试中控制时间、无法替换为 deterministic clock、无法跨时区做语义校验 -
sql.NullString是database/sql的实现细节,它要求你必须用Scan/Value,导致 domain 实体无法被json.Unmarshal正确解析(内部字段不可导出) - 正确做法:用基础类型封装,比如
CreatedAt int64(Unix timestamp),或自定义类型type CreatedAt string(ISO8601 格式字符串),把转换逻辑下沉到internal/adapter或internal/infrastructure - 若必须保留
time.Time,至少定义type Clock interface { Now() time.Time },由外层注入,禁止在 domain 内直接调用time.Now()
repository 接口为什么必须定义在 internal/domain 而非 internal/infrastructure
Go 没有包级可见性控制,依赖方向全靠目录约束。接口放在 infrastructure 就等于让 usecase 主动 import 实现层,彻底违反依赖倒置原则。
- 错误路径:
internal/infrastructure/mysql_user_repo.go定义UserRepository接口 →internal/usecaseimport 它 → 依赖箭头从 usecase 指向 infrastructure - 正确路径:
internal/domain/user_repository.go定义接口 →internal/infrastructure/mysql_user_repo.go实现它 →internal/usecase只 importdomain,不感知 MySQL - 接口方法名必须是业务语义,如
FindByEmail(email string) (*User, error),禁止暴露QueryRaw、Exec、*sql.Rows等 SQL 细节 - 返回值只能是 domain 类型(
*User)或 error,绝不返回gorm.DB或redis.Conn
cmd/main.go 里写 db.Init() 或 viper.ReadInConfig() 就算越界
cmd 是启动入口,不是配置中心或初始化脚本集合。任何具体实现的初始化代码,都会让单元测试无法绕过文件系统或网络依赖。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 常见错误:
cmd/api/main.go里直接调用viper.ReadInConfig()→ 测试时go test ./...报 “no such file” - 正确做法:所有初始化下沉到
internal/app,cmd/main.go只做三件事 —— 解析 flag、调app.New().Run()、返回 error - 禁止
cmdimportinternal/infrastructure或internal/repository;如果发现 IDE 提示 “cannot find package”,先检查go.modmodule 名是否与实际路径一致 - 配置加载应作为
app构造函数参数传入,例如func New(cfg Config) *App,而非在New()内部硬编码读取逻辑
pkg/ 不是“工具函数大杂烩”,而是可独立测试的无业务依赖模块
pkg 下每个子包必须满足:能单独执行 go test 通过,且 go list -f '{{.Deps}}' ./pkg/xxx 不包含任何 internal 或业务相关路径。
- 允许:
pkg/idgen(生成 UUID)、pkg/jsonx(带 context 的 JSON 编解码)、pkg/timefmt(统一时间格式化) - 禁止:
pkg/userutil里出现CalculatePoints;pkg/dbhelper里出现GetUserByID;这类逻辑属于internal/usecase或internal/infrastructure - 如果某个工具函数需要访问 domain 类型(比如加密用户邮箱),说明它不该在
pkg,而应移至internal/adapter作为协议适配器 - 性能提示:
pkg层函数尽量避免分配,如jsonx.Marshal应复用bytes.Buffer,而不是每次 new
最常被忽略的不是哪一层该放什么,而是 interface 的粒度 —— 不是每个 usecase 都要接收全部 repository,而是按需组合最小接口。比如 CreateOrderUseCase 只需要 OrderRepository 和 PaymentClient,别把整个 DB 或 Cache 注入进去。否则,哪怕 domain 层再干净,application 层也早已被耦合绑架。

















