项目结构应清晰分层:controllers、services、models 职责分明,新增api时文件增减路径可预期;main.go仅初始化和路由注册;routes按业务拆分;services依赖接口而非具体db;middleware单一职责;utils不含业务逻辑。

直接说结论:项目结构不是越深越好,而是要让 controllers、services、models 三层能清晰分隔职责,且新增一个 API 接口时,文件增减路径可预期、不跨层跳转。
为什么不能把所有 handler 写进 main.go
main.go 只该做初始化和路由注册,写业务逻辑会快速变成“上帝文件”。一旦需要加个用户登录接口,就得在 main.go 里塞参数校验、DB 查询、状态判断——这些本该由不同层级承担。
- 测试困难:无法单独 mock 数据库或 service 层,只能走端到端测试
- 复用率低:同样的用户查询逻辑,换一个路由或协议(比如 gRPC)就得重写一遍
- 并发安全风险:直接在 handler 里操作全局
db实例,容易漏掉 context 超时控制或事务边界
routes/ 目录里别只放一个 routes.go
单文件路由注册在 5 个接口以内还行,超过 10 个后,r.GET 和 r.POST 会挤成面条,且无法按业务域隔离中间件。比如 /api/v1/user 和 /api/v1/order 需要不同的 auth 级别,但共用一个文件就很难局部控制。
- 推荐拆法:
routes/user_route.go、routes/order_route.go,每个文件导出一个函数如SetupUserRoutes(r *gin.RouterGroup) - 路由组前缀必须由调用方传入,不要在文件内部硬编码
r.Group("/user")—— 否则 v2 版本升级时得改所有子路由文件 - 避免在 route 文件里 import controller 包以外的任何业务包(比如别直接 import service 或 model),否则破坏了分层依赖方向
services/ 里别暴露 *gorm.DB 或 *sql.DB
service 层应该只依赖接口,而不是具体数据库驱动。否则单元测试时,你得启动真实 MySQL,或者用 sqlmock 绕一大圈 mock 连接对象。
- 正确做法:定义
UserRepo接口,含FindByID(ctx, id) (*User, error)方法;service 层只接收该接口作为参数 - repo 实现放在
repos/目录下(而非 models),models 只放纯结构体定义 - 如果项目初期图快直接传
*gorm.DB,后面加 Redis 缓存层或迁移到 MongoDB 时,service 层代码几乎全部重写
middleware/ 不是垃圾桶,别堆日志、鉴权、CORS 全塞一起
一个中间件文件里混写多个职责,会导致复用性差、调试难。比如 auth.go 里既做 JWT 解析又写请求计数,下次给管理后台加独立鉴权时就得复制粘贴再删一半。
- 每个中间件文件专注一件事:
jwt_auth.go、rate_limit.go、cors.go - 中间件函数名要体现作用范围,比如
AdminAuth()和UserAuth()明确区分权限粒度,而不是笼统叫Auth() - 注意
c.Next()的位置:鉴权类中间件必须在c.Next()前 abort,而日志类必须在之后读取 status code —— 放反了会导致 status 总是 0 或 panic
真正容易被忽略的是:utils/ 目录里别放业务逻辑工具,比如 “生成订单号” 或 “计算折扣” 应该属于 service 层;utils 只该有时间格式化、base64 编解码、HTTP 客户端封装这类与业务无关的通用能力。否则半年后你会在三个地方看到一模一样的折扣计算函数。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











