Clean Architecture在Go中靠import路径、字段类型和接口位置判断是否合规:domain层禁用time.Time和sql.NullString,须用string或指针替代并抽象Clock接口;repository接口必须定义在domain或usecase层;cmd/main.go仅组装依赖。

直接说结论:Clean Architecture 在 Go 里不是靠目录名堆出来的,而是靠 import 路径是否越界、结构体字段是否含 time.Time 或 sql.NullString、接口定义位置是否在 domain 层来判断的——写错一个地方,整层就失效。
domain 层为什么不能出现 time.Time 和 sql.NullString
因为 domain 是业务核心,必须和数据库、标准库解耦。一旦你写 CreatedAt time.Time,就等于把领域模型绑死在 time 包上;写 Status sql.NullString,等于强制所有仓库实现都得用 database/sql 的扫描逻辑,mock 测试时连 json.Unmarshal 都会失败(sql.NullString 内部字段不可导出)。
正确做法是:
- 时间字段用
CreatedAt string或自定义类型type CreatedAt time.Time(不直接用标准库别名) - 空值统一用原生指针:
*string、*int64,而不是sql.NullString - 需要时间行为?定义
type Clock interface { Now() time.Time },由外层注入 - ORM tag(如
gorm:"column:email")只能出现在internal/adapter或internal/infrastructure的映射结构体里
repository 接口必须定义在 domain 或 usecase 包里
Go 没有包可见性控制,依赖方向全靠目录约束。如果把 UserRepository 接口放在 infrastructure 里,usecase 就得 import infrastructure,箭头反了,违反依赖倒置。
立即学习“go语言免费学习笔记(深入)”;
常见错误现象:
- 测试时 panic “undefined: UserRepository”,因为
usecase找不到接口定义 - 想换 MySQL 为 SQLite,结果发现
usecase直接 import 了github.com/go-sql-driver/mysql
正确结构:
-
domain/user_repository.go定义type UserRepository interface { FindByID(id string) (*User, error) } -
internal/infrastructure/mysql_user_repo.go实现该接口,import domain,但domain绝不 import 它 - 返回值必须是
*User(domain 实体),不能是*sql.Rows或gorm.DB
cmd 目录里只允许三行有效代码
cmd/api/main.go 的唯一职责是解析 flag、调 app.Run()、返回 error。多写一行初始化代码,比如 viper.ReadInConfig() 或 mongo.Connect(),就会让单元测试绕不开文件系统或网络——这不是启动脚本,是测试雷区。
容易踩的坑:
- 在
main.go里初始化 Redis 客户端,导致go test ./...报 “connection refused” - 把路由注册写在
main.go,结果internal/api包开始 importgin,service 层没法复用于 CLI 或 gRPC - 配置加载没抽象,环境变量硬编码,本地开发和 CI 环境行为不一致
正确做法:
- 所有初始化下沉到
internal/app.NewApp(),它接收配置结构体、DB 客户端、clock 实例等依赖 -
cmd/api/main.go只保留:cfg := loadConfig()、app := app.NewApp(cfg, db, clock)、os.Exit(app.Run()) -
cmd绝对不能 importinternal/infrastructure或任何带框架/DB 的包
真正难的不是分层,而是每次写 struct 字段、加 import、定义接口时,都要下意识问一句:“这个东西,能让 domain 层在不引入 net/http 或 database/sql 的前提下独立编译和测试吗?” —— 这个问题答错一次,整洁架构就裂一道缝。


















