Go项目必须严格遵循internal四层单向依赖、配置分环境叠加、复用代码放pkg等规范,否则将导致逻辑失控、维护困难和环境配置错误。

internal 四层必须单向依赖,否则编译不报错但逻辑失控
Go 的 internal 目录不是装饰,是语言级访问控制机制:外部模块无法 import 任何 internal 下的包。这意味着你把 handler、service、repository、model 全塞进去,只是物理隔离;真正起作用的是依赖方向。
常见错误现象:service 层直接 new 一个 handler 实例去调用响应方法;repository 里写 if user.Status == "active" 这类业务判断;model 包里定义 func (u *User) SendWelcomeEmail() —— 这些都会让分层失效,后续改一个字段要 grep 十个文件。
- handler 只做三件事:解析
c.ShouldBindJSON()、校验validator.Validate()、调用s.Register() - service 不碰
*gin.Context,不构造gin.H{},所有错误返回error,由 handler 统一转 HTTP 状态码 - repository 方法名必须是动词开头:
CreateUser()、FindByPhone()、UpdateStatus(),禁止GetActiveUsers()这种带业务语义的命名 - model struct 字段必须与数据库列名严格一致,GORM 标签只写
gorm:"column:phone",不加json:"phone"或其他序列化标签(那是response层的事)
cmd/main.go 只负责启动,别在里面初始化 DB 或注册路由
把 DB 初始化、viper 配置加载、router 注册全堆进 main.go,短期省事,三个月后你会在 main.go 里翻 200 行代码找 Redis 超时设置。启动入口必须“薄”——它只串联已封装好的组件。
使用场景:CI/CD 构建时需要替换不同环境配置;测试时想 mock DB 连接;灰度发布时需动态加载中间件开关。
立即学习“go语言免费学习笔记(深入)”;
-
cmd/api/main.go只 import"project/internal/app"和"project/internal/router"两个包 -
app.Init()返回*App结构体,内含DB、Redis、Config字段,供 router 和 handler 按需取用 - router 初始化必须接收
*gin.Engine和*App,而不是在函数内部自己 new gin.Default() - 禁止在
main.go里写r.POST("/user", handler.CreateUser)—— 路由注册逻辑应放在internal/router/v1.go中
configs 必须按环境叠加覆盖,viper.SetConfigName("app") 是陷阱
viper.SetConfigName("app") 加 viper.AddConfigPath("configs") 看似简洁,但会导致 dev.yaml 里的 db.timeout: 5s 被 app.yaml 里的 db.timeout: 30s 覆盖掉 —— 因为 viper 默认只读一个文件。生产环境连不上 DB 就是因为这个。
参数差异:viper.MergeConfigMap() 适合单元测试注入 mock 配置;viper.ReadInConfig() 适合单文件场景;多环境必须用 viper.MergeConfig() 手动加载顺序。
- 先
viper.SetConfigFile("configs/app.yaml"),调viper.ReadInConfig() - 再根据
os.Getenv("ENV")加载configs/dev.yaml或configs/prod.yaml,用viper.MergeConfig()合并 - 配置结构体定义必须独立在
pkg/config/config.go,避免main.go和service/user.go各自定义一套type Config struct - 所有 config 字段必须有默认值或 panic 提示,例如
DBTimeout time.Duration `mapstructure:"timeout" default:"30s"`
pkg 下放真复用代码,别把 middleware 塞进 internal
把 JWT 验证中间件放在 internal/middleware/auth.go,看起来“归类清晰”,但下次写另一个项目时,你得复制粘贴、改 import 路径、修 token 字段名——这不是复用,是克隆。真正的复用是能 go get github.com/yourname/pkg/jwt 直接用。
容易踩的坑:把日志中间件 logger.go 放 internal,结果每个项目都要重写 zap 字段格式;把分页工具 Paginate() 写在 internal/utils,结果 service 层调用时发现它依赖了 GORM 的 *gorm.DB,根本没法抽到 pkg。
-
pkg/middleware里只放纯函数:func JWTAuth(secret string) gin.HandlerFunc,参数全是基础类型,不依赖项目内任何 struct -
pkg/config提供NewViper() *viper.Viper和LoadFromDir(dir string) (*Config, error),不绑定具体 YAML 结构 -
pkg/response定义Success(data interface{}) gin.H和Error(code int, msg string) gin.H,不引用 model 或 service -
internal下的middleware目录只放项目特有逻辑,比如“检查用户是否绑定了企业微信”,这种没法复用的才放里面
internal/repository 层的事务边界——它不该决定用不用 transaction,而该暴露 WithTx(func(*gorm.DB) error) error 接口,让 service 层按业务需要控制。这点不提前约定,后期加支付、库存扣减这类强一致性操作时,几乎必然返工。


















