gin本身不限制目录结构,但生产级项目必须用internal四层分层(handler→service→repository→model),否则半年后你会在main.go里翻300行路由注册代码,改个字段要grep全项目。

直接说结论:Gin本身不限制目录结构,但生产级项目必须用internal四层分层(handler→service→repository→model),否则半年后你会在main.go里翻300行路由注册代码,改个字段要grep全项目。
为什么必须把业务代码放进internal目录
Go语言规定:任何包路径中含internal的子目录,外部模块无法导入。这不是约定,是编译器强制检查——哪怕你写import "myapp/internal/handler",go build直接报错。
这意味着:
- 业务逻辑不会被其他项目意外引用(比如测试包、CLI工具误调用service)
- 重构时能放心重命名
service包,IDE不会提示“该包被pkg/utils引用”这种虚假依赖 -
go list ./...默认不扫描internal,CI跑单元测试时天然隔离
handler层只做三件事,多一行都算违规
它不是“业务入口”,而是HTTP协议适配器。常见错误包括在handler里写SQL、拼JSON响应体、处理事务回滚逻辑。
正确做法:
- 用
c.ShouldBindJSON(&req)解析参数,校验走binding标签或自定义Validate()方法 - 调用
service.UserRegister(req),传入纯净struct,不传*gin.Context - 收到
service返回的dto.UserDTO后,用c.JSON(201, resp)吐出HTTP响应
典型反例:handler/user.go里出现db.Create(&user)或log.Info("用户注册成功")——这些必须下移到service或middleware。
路由注册别堆在main.go,用版本分组+模块文件拆分
Gin 1.9.0起支持router.Group("/v1")嵌套,但很多人只用到第一层,结果routers.go变成2000行的意大利面条。
推荐结构:
-
routers/router.go:只做初始化gin.New()、挂载全局中间件(logger、recovery)、调用v1.RegisterRoutes(r) -
routers/v1/user.go:定义func RegisterRoutes(r *gin.RouterGroup),内部用rg := r.Group("/users")再细分 -
routers/v1/todo.go:同理,和user.go完全解耦,增删模块不影响彼此
这样改需求时,删掉todo.go文件即可下线整个TODO模块,不用在长文件里找rg.POST("/todos", ...)那行再注释掉。
configs加载顺序错一个字,dev环境就用prod数据库
用viper时,必须按顺序调用:
err := viper.ReadInConfig() // 读app.yaml
viper.SetConfigName("dev") // 再读dev.yaml覆盖
viper.MergeInConfig() // 否则db.timeout等基础配置会丢失
容易踩的坑:
- 把
viper.SetConfigFile("dev.yaml")写成绝对路径,导致CI里找不到文件 - 在
main.go里直接viper.GetString("db.host"),而不是抽成pkg/config.Config结构体——后续加新配置项时所有地方都要改 - 没设
viper.AutomaticEnv(),导致K8s里用DB_HOST环境变量覆盖失败
真正关键的不是目录名,而是internal是否真的隔绝了依赖、handler是否干净得只剩三行、路由文件是否能独立删除——这些点漏掉任何一个,目录结构再漂亮也是纸糊的架子。











