应该将真正跨模块共享且生命周期与应用一致的全局变量(如log、config、db)放在internal/variable.go,避免放入临时结构体或请求级实例以防止并发安全问题和状态污染。

全局变量该不该放 internal/variable.go?
应该,但仅限真正跨模块共享、生命周期与应用一致的实例。比如 Log、Config、DB 这类——它们初始化一次,后续所有 handler、middleware、task 都要调用,且不能被重置。
常见错误是把业务逻辑里临时用的结构体、缓存 map、甚至单次请求的参数验证器也塞进全局变量。这会导致并发安全问题或状态污染。
-
BasePath可以放,它是只读常量,启动时确定后不再变 -
DB必须放,GORM*gorm.DB是线程安全的,但必须确保只初始化一次 -
Config(Viper 实例)必须放,否则每个模块重复解析 YAML 会浪费资源、且可能读到不同版本 - 别把
redis.Client或rocketmq.Producer直接当全局变量裸奔——应封装成带连接池管理的 service 实例,再挂到全局
配置文件怎么加载才不会在测试/部署时出错?
用 Viper 加载 config.yaml 时,默认行为不区分环境、不 fallback、不校验字段,极易导致线上 panic 或本地调试失败。
正确做法是显式指定路径 + 环境前缀 + 强类型绑定:
- 启动时通过命令行 flag 或环境变量传入
--env=prod,而不是硬编码viper.SetEnvPrefix("APP") - 调用
viper.AddConfigPath("./config")后,立刻viper.SetConfigName("config"),再viper.SetConfigType("yaml") - 必须调用
viper.ReadInConfig()并检查 error;若失败,应 panic 或 log.Fatal,不能静默忽略 - 用结构体绑定(
viper.Unmarshal(&cfg))而非viper.GetString()拼接——后者容易漏字段、类型错、没默认值
示例中 system.port 若未定义,结构体字段带 default:"8080" tag 就能兜底,而裸调 viper.GetInt("system.port") 会返回 0。
为什么 DB 全局变量初始化后还要调用 DB.AutoMigrate()?
因为 DB 全局变量只是连接句柄,不自动建表。GORM 的 AutoMigrate 是运行时行为,必须显式触发,且只应在启动阶段执行一次。
容易踩的坑:
- 在 handler 里反复调
DB.AutoMigrate(&User{})—— 每次请求都连 DB 执行 DDL,性能爆炸,还可能锁表 - 没加锁就多 goroutine 并发调
AutoMigrate—— 虽然 GORM 内部有简单保护,但不同 model 之间无序执行仍可能冲突 - 把 migration 放在
init()函数里 —— 此时 Viper 配置还没加载,DSN 为空,gorm.Open直接 panic
标准流程是:加载配置 → 初始化 DB → 在 initialize/db.go 中统一调一次 DB.AutoMigrate() → 把 DB 赋给全局变量。
多个服务共用一套配置时,Config 全局变量怎么避免互相覆盖?
如果项目拆成 user-web、order-api 等多个 Gin 服务,又共用同一套 Nacos 配置中心,每个服务的 Config 全局变量必须隔离初始化,不能复用同一个 Viper 实例。
原因很简单:Viper 的 WatchRemoteConfig 是全局监听,一个服务 reload 配置,其他服务的 Config 实例也会同步变更——但它们的 struct 绑定字段可能不同,导致 panic 或读错值。
- 每个服务启动时,各自 new 一个 Viper 实例,各自
AddRemoteProvider - 不要在 common lib 里 export 一个全局
var Config *viper.Viper,那是反模式 - 如果真要复用配置解析逻辑,抽成函数(如
LoadConfig(env string) (*ConfigStruct, error)),而不是复用 Viper 实例
真正的“全局”只存在于单个进程内。跨进程的配置同步,靠的是远程 provider(如 Nacos)推送 + 各自进程独立 reload,不是靠 Go 变量共享。











