Gin项目必须在internal下按handler→service→repository→model四层单向依赖组织,否则后期改一个接口要翻遍main.go、查三处逻辑、修两个循环引用,不是重构,是重写。

直接上结论:Gin项目必须在internal下按handler→service→repository→model四层单向依赖组织,否则后期改一个接口要翻遍main.go、查三处逻辑、修两个循环引用,不是重构,是重写。
为什么不能把所有 handler 写进 router.go 或 main.go
常见错误现象:router.go里混着参数校验、DB调用、返回构造;main.go超过800行,git blame显示12人改过同一函数;新增字段时发现user.go在handler和model里各定义了一次,字段名还不一致。
根本原因在于Go没有运行时反射约束,编译器不拦你跨层调用。但internal目录是硬隔离——外部包无法导入internal/handler,更别说反向调service了。一旦放任代码挤进main.go或router,等于主动放弃这道防线。
实操建议:
- 删掉
router/router.go里所有业务逻辑,只留r.GET("/user", userHandler.GetUser)这类纯映射 -
handler层函数签名统一为func(c *gin.Context),不接收*sql.DB或redis.Client - 所有
binding校验必须在handler完成,service层只接收已校验的结构体
internal/service 里写 SQL 就是错的
典型错误:在service/user.go里直接调db.Where("status = ?", 1).Find(&users),或者拼fmt.Sprintf条件语句。
这样做的后果是:单元测试无法mock数据库行为;换ORM(比如从GORM切到Ent)要改所有service文件;加个缓存层得在5个service方法里重复写cache.Get/cache.Set。
正确做法是让repository承担数据访问职责:
-
repository方法名必须是动词开头:CreateUser、FindByPhone、UpdateStatusByID -
service调用repo.FindByPhone(phone),但绝不关心这个方法内部是查MySQL还是Redis - 如果某查询需要连表+分页+全文检索,不要塞进
repository,拆成repository组合调用,或单独建query包
viper.LoadConfigFile 加载顺序错了会静默失效
错误配置方式:viper.SetConfigName("dev"); viper.AddConfigPath("configs"),结果app.yaml里的database.timeout没生效,日志里全是context deadline exceeded。
原因在于viper的覆盖逻辑:后加载的配置项会覆盖先加载的同名项,但dev.yaml通常只覆盖app.yaml里的env或debug字段,漏掉数据库连接池大小、超时等关键项。
必须按顺序加载:
- 先
viper.SetConfigName("app"); viper.AddConfigPath("configs") - 再
viper.SetConfigName(os.Getenv("ENV")); viper.AddConfigPath("configs") - 最后
viper.ReadInConfig()
别把viper.Unmarshal写在main.go里——抽成pkg/config/config.go的NewConfig()函数,确保全项目只解析一次配置树。
middleware 和 router 目录容易变成“黑洞”
常见问题:middleware/auth.go里悄悄调用了service.UserLogin,导致handler层想复用该中间件时,被迫引入整个service包;router/api_v1.go里给某个路由加了JWTAuth中间件,但忘了在router/admin.go里加,结果后台接口裸奔。
关键点在于作用域分级:
- 全局中间件(如
logger、recovery)放middleware/global/,注册在gin.Engine上 - 模块级中间件(如
admin.Auth、api.RateLimit)放middleware/api/,由对应Group调用 -
router目录下每个文件只注册自己模块的路由,且必须显式声明中间件:v1.Use(middleware.APIAuth()),不依赖隐式继承
最易被忽略的是中间件的错误处理路径——c.Abort()之后,后续中间件和handler不会执行,但defer里的日志可能仍会打印“请求成功”,这种时序陷阱比语法错误更难调试。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











