cmd 目录只放极简入口,每个子目录对应一个可执行文件,main.go 仅调用 app.run();业务逻辑、初始化、配置等须抽至 internal/app 或 internal/adapter;禁止 cmd import 领域层以外包,避免依赖倒置。

Go 项目里 cmd 目录该放什么,不该放什么
它只该放极简的程序入口,不是业务逻辑中转站。很多团队把 main.go 塞满初始化、配置加载、中间件注册,结果一测单元测试就崩——因为 main 包依赖了太多业务层。
-
cmd下每个子目录对应一个可执行文件(如cmd/api、cmd/migrate),每个只含main.go和必要 flag 解析 - 所有服务启动逻辑(DB 连接池创建、gRPC server 初始化、HTTP 路由注册)必须抽到
internal/app或internal/adapter中,main.go只调用一个app.Run()函数 - 禁止在
cmd里 importinternal/domain以外的领域层以外的包;否则 Clean Architecture 的依赖方向就反了
为什么 internal/domain 不能有 time.Time 或 sql.NullString
领域模型一旦引入框架或数据库类型,就锁死了持久化方式,也断了单元测试的退路。你没法用内存仓库 mock 一个带 sql.NullString 的结构体,因为它的 Scan/Value 方法绑死了 database/sql。
- 所有时间字段统一用
string(ISO8601 格式)或自定义类型如type CreatedAt string,转换逻辑下沉到internal/adapter - 空值语义用 Go 原生指针(
*string)或optional模式(如type Email struct{ value *string }),不暴露 SQL 层细节 - 如果用了 ORM(如 GORM),它的 tag(
gorm:"column:name")只能出现在internal/adapter/repository的映射结构体里,绝不出现在domain
go mod tidy 后 go.sum 疯涨,是不是架构出问题了
不一定,但大概率是 internal/infrastructure 引入了重型 SDK(比如 AWS SDK v2、Datadog agent),而它们又拉了一堆间接依赖。Clean 架构本意是让基础设施可插拔,但现实是很多人把“可插拔”理解成“全量引入”。
- 检查
go list -m all | grep aws,确认是否真需要整个github.com/aws/aws-sdk-go-v2;通常只需service/s3或service/dynamodb子模块 - 用
replace在go.mod中锁定轻量替代:比如用minio/minio-go替代 S3 SDK,或用redis/go-redis替代github.com/go-redis/redis/v8(后者带 context 包膨胀) -
go.sum增长本身不危险,但若其中出现golang.org/x/tools或google.golang.org/grpc/cmd/protoc-gen-go-grpc这类开发期工具,说明 build 时误把 dev 依赖打进生产镜像了
HTTP handler 怎么写才不算污染 internal/adapter
handler 是适配器,不是控制器。它只做三件事:解析请求、调用 usecase、序列化响应。任何日志打点、鉴权跳转、错误码映射都不该在这里写死。
立即学习“go语言免费学习笔记(深入)”;
- 鉴权交给独立 middleware(放在
internal/adapter/http/middleware),且 middleware 只能依赖internal/port接口,不能碰domain实体 - 错误处理统一走
apperror类型(如type AppError struct{ Code int; Message string }),由顶层 handler 中间件转 HTTP 状态码,而非每个 handler 写if err != nil { w.WriteHeader(400) } - 路径参数、query、body 解析后应立刻转成 usecase 输入结构体(如
CreateUserInput),不要在 handler 里做字段校验或默认值填充——那是internal/usecase的事
type ErrInvalidEmail interface{ error }),而不是 struct,否则下游无法用 errors.Is 判断;这点在跨 service 调用时会突然卡住。










