单体 Gin 项目的目录分层规范
一句话结论:单体 Gin 项目分层目标是“依赖单向、职责清晰、可测试、不循环导入”。推荐:cmd 放唯一入口做组装;internal 下按 config/model/dao/service/handler/middleware/router 分层,依赖方向固定为 handler→service→dao→model,中间件只做横切;错误码、响应、日志等放 common。main 里“配置→日志→DB/Redis→service→router→server”手写依赖注入(构造器逐层传参),不用全局变量与魔法 DB。这套结构参考了 Go 官方 internal 约定与常见 project-layout,但不必教条,团队一致即可。
目录结构
cmd/server/main.go # 入口:读配置、组装依赖、启动+优雅退出
internal/
config/ # Config 结构体与 viper 加载(见配置章节)
model/ # 表结构/实体,全层共享,避免循环导入
dao/ # 数据访问(GORM/Redis 封装),只碰 model
service/ # 业务逻辑/事务编排,只依赖 dao 接口
handler/ # Gin handler:绑定/校验/调 service/组响应
middleware/ # 认证、日志、限流、恢复等横切逻辑
router/ # 路由注册与中间件装配顺序
common/ # errcode、resp、logger 等基础设施
configs/ # 各环境 yaml
分层规则
| 层 | 职责 | 依赖 | 禁止 |
|---|---|---|---|
| handler | 参数绑定校验、调用 service、统一响应 | service、common | 拼 SQL、写业务规则 |
| service | 业务规则、事务、领域逻辑 | dao、model | 直接碰 gin.Context |
| dao | 数据访问细节 | model | 业务分支 |
| model | 数据结构 | 无 | 业务逻辑 |
- 接口放哪:dao 层接口由 service 定义并注入,测试时 service 用 fake dao,不用引 mock 框架也能单测。
- main 组装:NewService(dao) / NewHandler(svc) / NewRouter(handler, mw) 一路构造注入;gin.Engine 只在 router/main 出现。
- 命名与卫生:internal 防止被外部包导入;utils 别做成垃圾桶;handler 要薄、service 要厚(薄 controller 厚 service);没有 import cycle(跨层共享一律走 model/common)。
为什么这么分(反问式自检)
- 能不改 SQL 就替换存储实现吗?——能,dao 接口隔离。
- 能对 service 写纯单测吗?——能,依赖是接口。
- 换框架(Gin→Echo)影响多大?——只动 handler/router 层,业务与数据不动。
追问记忆点
- 追问:为什么 handler 不能直接 import dao?——会让业务逻辑散落、测试困难、依赖方向混乱;必须经 service 编排。
- 追问:接口定义在消费方还是实现方?——Go 惯例是消费方(service)定义所需最小接口,实现方(dao)被动满足,避免反向依赖。
- 追问:一定要 wire/依赖注入框架吗?——单体手写构造函数足够,显式、可读、可编译期检查;复杂微服务再考虑容器。
- 记忆点:入口组装、分层单向依赖、接口解耦、model/common 共享、无循环导入;薄 handler + 厚 service。