gin控制器层只做收参、校验、转调service三件事;sql、http调用、json序列化等应移至service层,通过接口依赖实现可测试、可替换、可复用。

分层设计不是为了套概念,而是让业务逻辑能被单独测试、替换和复用。Gin 本身不强制结构,但一旦 Controller 开始写 SQL、拼 JSON、调第三方 API,就该拆了。
Controller 层只做三件事:收参、校验、转调 Service
常见错误是把 db.Where(...).First()、http.Post(...)、json.Marshal(...) 全塞进 GetUser 函数里。这导致无法对业务逻辑做单元测试,也难以复用同一段用户查询逻辑到定时任务或 CLI 命令中。
- Controller 只负责从
c.Param/c.ShouldBind拿数据,用validator或自定义requests.UserRequest结构体校验 - 所有业务判断(如“状态为 0 则拒绝”)移入 Service 方法,Controller 只做 error 分类:400/404/500
- 返回值统一走
c.JSON,不拼 HTML、不写文件、不发邮件
Service 层必须依赖接口,不能直接 new struct
如果 UserServiceImpl 直接 new 了 mysql.UserRepo 和 redis.CacheClient,那这个 Service 就没法在测试时 Mock 数据库行为,也无法切换为内存存储做本地调试。
- 先定义接口:
type UserRepository interface { FindByID(id uint) (*User, error) } - Service 构造函数接收接口:
func NewUserService(repo UserRepository, cache CacheService) *UserService - 实际初始化时才注入具体实现,比如在
main.go或 DI 容器里传&mysql.UserRepo{DB: db}
Router 分组 + 独立注册文件,避免 main.go 膨胀
把所有 r.GET("/users", handler.GetUser) 堆在 main.go 里,很快就会失控。Gin 的 router.Group 是天然的模块切分点,但更关键的是路由注册逻辑要抽离。
- 每个模块建独立文件,如
routes/user_route.go,导出func SetupUserRoutes(r *gin.RouterGroup) - 在
main.go中按需调用:SetupUserRoutes(r.Group("/v1")),而不是把所有路由硬编码进去 - 中间件也按组绑定:
v1.Use(authMiddleware, logMiddleware),避免全局污染
Model 和 Repo 不等于数据库表一对一映射
很多人一上来就让 User struct 字段和 MySQL user 表完全一致,加字段就改 struct,删字段就删字段——结果 Service 层拿到的 model 里混着加密字段、缓存标记、前端不需要的关联数据,越往后越难维护。
-
models.User应只含领域核心字段(ID、Name、CreatedAt),不含密码哈希、token、status_text 等衍生字段 - Repo 层返回的应是 clean model;需要额外字段时,由 Service 组装 DTO 或用专用响应结构体(如
responses.UserDetail) - 数据库操作封装在 Repo 接口内,Service 不知道用的是 MySQL 还是 SQLite,甚至可以 mock 返回固定数据用于测试
真正容易被忽略的不是“要不要分层”,而是每一层的边界是否真的被守住——比如 Service 方法里偷偷调了 c.Redirect,或者 Repo 层开始写日志,这种破窗效应会迅速瓦解整个分层价值。











