clean architecture 在 go 中依赖 import 路径与包职责固化依赖:domain 层禁用 time.time 和序列化 tag,须抽象 clock 接口;dto/adapter 层承载所有 tag;repository 接口定义于 domain/usecase;handler 仅作协议胶水,严禁业务逻辑。

Go 里 Clean Architecture 不是目录套目录,而是靠 import 路径和包职责把依赖箭头真正“钉死”——你写错一个 import,架构就塌一半。
domain 层为什么不能出现 time.Time 或 json:"id"
因为 domain 是业务核心,不是序列化容器。一旦你写 CreatedAt time.Time 或 json:"id",它就同时承担了时间语义、HTTP 序列化、数据库映射三重职责,后续换 gRPC、换 MongoDB、加审计日志全得改 struct。
- 时间字段必须抽象为接口:
Clock,由外层注入;domain 内只用CreatedAt string或自定义类型type CreatedAt time.Time(不直接用标准库) - 所有 tag(
json:、db:、protobuf:)只能出现在 DTO 或 adapter 层的结构体里,比如user_http_request或user_db_model - 空值统一用 Go 原生指针:
*string,而非sql.NullString;后者会污染测试构造逻辑,mock 仓库时连json.Unmarshal都失败
repository 接口该定义在哪个包
必须定义在 domain 或 usecase 包里,绝不能放在 infrastructure 或 adapter 中。
- 反例:
infrastructure/mysql/user_repository.go里定义UserRepository接口 →usecase不得不import infrastructure,依赖倒置彻底失效 - 正例:在
domain/user_repository.go定义interface UserRepository { FindByEmail(email string) (*User, error) },然后infrastructure/mysql实现它并import domain - 接口方法名要业务语义化:
Store、FindByEmail✅;Insert、QueryRaw❌
HTTP handler 怎么写才不算污染 usecase
handler(或 controller)只是协议胶水,它唯一合法动作是:解析请求 → 调用 usecase → 格式化响应。任何业务判断、错误码映射、中间件逻辑都不得越界。
-
usecase.CreateUser返回domain.ErrEmailAlreadyExists,不是http.StatusConflict;状态码转换只发生在 handler 层 - handler 构造函数只接收 usecase 接口,例如
func NewCreateUserHandler(usecase CreateUserUsecase),不 import 任何 infra 包 - 禁止在 usecase 方法签名里出现框架类型:
service.Register(c *gin.Context)❌;service.Register(input CreateUserInput)✅ - DTO 和 domain 实体之间必须显式单向转换:
mapToDomain()和mapToResponse(),不嵌套、不复用 struct、不共享指针
cmd/ 目录里能写什么
只允许三行有效代码:flag 解析、app.Run()、error 返回。多一行初始化逻辑,单元测试就多一分不可控。
- DB 连接池、配置加载、路由注册、中间件挂载 —— 全部下沉到
internal/app或internal/bootstrap - 禁止在
cmd/api/main.go里import internal/infrastructure;否则 domain 就被 infra 反向绑架 - 如果
go test ./报 “cannot load config: no such file”,大概率是main.go里硬写了viper.ReadInConfig()
最常被忽略的其实是时间抽象和 DTO 转换——它们不报错、不崩溃,但会在三个月后让你改遍所有测试用例和迁移脚本。











