go clean architecture核心是解决http handler塞sql、service依赖gin.context、测试需启数据库等真实问题:service层禁用net/http和gin.context以保障复用性与可测性,pkg/仅放可独立测试的工具代码,repository接口必须定义在domain层以确保依赖倒置,jwt中间件属于adapter层且不可查db。

Go网络项目用Clean Architecture不是为了显得“高级”,而是为了解决真实问题:HTTP handler里塞SQL、service层直接调用gin.Context、测试时不得不启动数据库——这些都会在加功能、换框架、写单元测试时集中爆发。
为什么internal/service不能import net/http或gin.Context
service层一旦依赖HTTP相关类型,就等于把业务逻辑和协议绑定死了。比如你写了个CreateOrderService,参数是*gin.Context,那它就无法被CLI命令、gRPC入口、定时任务复用。
- 正确做法:service只接收DTO(如
CreateOrderInput)和domain实体,返回error或domain.Order - 错误现象:IDE提示“cannot use *gin.Context as argument”;go test报错“undefined: gin”
- 性能影响:看似少写一层转换,实则让整个service失去可测试性——你得mock整个HTTP栈才能跑一个订单创建逻辑
pkg/目录里放什么、不放什么
pkg/是项目里唯一能被所有层安全引用的“工具箱”,但它必须干净到能独立go test通过。
- 可以放:
idgen.New()、time.Clock接口实现、jsonx.Marshal封装 - 严禁放:
pkg/db(这是infrastructure)、pkg/auth/jwt.go(含jwt.Token依赖,属于adapter层)、任何带internal/import路径的文件 - 常见错误:把
pkg/config里写viper.Unmarshal(&cfg)后直接returncfg.DB——这等于把数据库配置暴露给了domain层
repository接口该定义在domain/还是internal/usecase/
必须定义在domain/(或internal/model/)。Go没有包级访问控制,靠目录位置强制依赖倒置。
- 如果定义在
internal/infrastructure/mysql_user_repo.go,那usecase就得import infrastructure——依赖箭头朝外,架构就塌了 - 正确结构:
domain/user_repository.go里写type UserRepository interface { FindByID(id string) (*User, error) },然后internal/infrastructure/mysql_user_repo.go实现它 - 关键细节:接口方法返回值只能是
domain.User或error,绝不能是*sql.Row或mongo.Cursor
HTTP中间件(如JWT)该放在哪一层
JWT验证属于“协议适配”,必须落在interface adapters层,具体是api/middleware/或internal/api/handler.go里。
- 它能读
Authorizationheader、解析token、把userID塞进context.Context,但绝不调用userRepo.FindByID——那是usecase的事 - 错误做法:在middleware里直接查DB做黑名单校验;或把
jwt.Parse逻辑写进usecase.Login - 容易被忽略的点:中间件提取的
userID应以string或domain.UserID形式传入handler,而不是原始jwt.Token对象
最常被跳过的环节是domain层的时间抽象——用time.Time字段看着省事,但一到测试时间敏感逻辑(比如“订单24小时内可取消”),你就得在每个test里重写time.Now,而正确的做法是在构造service时注入Clock接口。











