「配置文件是被解析成一个 config 模块下的全局变量吗?还是存到 svc 里?」—— 这篇把解析流程、存储归宿、以及为什么两者都不是一次讲清。
准确说法是:config 包只定义结构体的「类型」;真正的配置实例在 main.go 里被解析出来(局部变量),然后通过参数传进 svc,作为 svcCtx.Config 字段被业务读取。
所以:① config 模块里没有全局变量(只有 type Config struct);② 配置实例确实「住」在 svc 里(svcCtx.Config),但它是被显式传进去的,不是偷偷塞的全局;③ 因为启动后配置只读不变,从 svc 读等价于「全局只读」,却保留了可注入、可替换、可测的好处。
internal/config/config.go 里只有类型定义和零值,没有任何 var Config = ...。真正的那份值在 main 里 new 出来后传给了 svc。go-zero 用自带的 core/conf 包解析,默认读 YAML(也支持 JSON),启动时一次性加载。
// internal/config/config.go —— 只定义「类型」 package config import ("github.com/zeromicro/go-zero/rest") type Config struct { rest.RestConf // 内嵌:Name/Host/Port/Mode/Timeout... Mysql MysqlConf CacheRedis redis.RedisConf Auth struct { AccessSecret string AccessExpire int64 } } // main.go —— 真正解析的地方 func main() { var c config.Config // MustLoad:读 etc/user-api.yaml,反序列化进 c;失败直接 panic conf.MustLoad(*configFile, &c) // 想要环境变量覆盖 YAML,可加 conf.UseEnv(): // conf.MustLoad(*configFile, &c, conf.UseEnv()) svcCtx := svc.NewServiceContext(c) // ★ 传进去 ... }
etc/xxx.yaml,用自带 YAML 解析器按字段名 / json 标签映射到 struct。YAML 的 snake_case 自动对应 Go 的 CamelCase。${ENV_NAME} 会被环境变量替换,配合 conf.UseEnv() 可在部署时覆盖配置(K8s 里常用)。rest.RestConf / zrpc.RpcConf 会把 Name、ListenOn、Mode、Timeout、CpuThreshold 等框架字段一并解析进来。etc/user-api.yaml 长这样,字段名和 config 结构体一一对应:Name: user-api Host: 0.0.0.0 Port: 8888 Mode: dev # dev / test / pro Mysql: DataSource: root:123456@tcp(127.0.0.1:3306)/user?charset=utf8mb4&parseTime=true CacheRedis: - Host: 127.0.0.1:6379 Type: node Auth: AccessSecret: my-secret-key AccessExpire: 86400
两种候选都被否定了——看代码对照最清楚。
config 包里没有 var C Config 这种包级变量。如果真写成全局,所有包都能 import 改它,测试无法注入不同配置——正是 go-zero 想避开的。c 作为参数传给 svc.NewServiceContext(c),svc 保存为 Config config.Config 字段。它「住」在 svc,但身份是显式依赖。// internal/svc/servicecontext.go type ServiceContext struct { Config config.Config // ★ 配置以「字段」身份存这里,不是全局变量 DB *gorm.DB Redis *redis.Redis } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, // 把传入的副本收好 DB: gorm.Open(mysql.Open(c.Mysql.DataSource)), Redis: redis.New(c.CacheRedis), } }
所以严格回答你的问题:不是全局变量,但确实存进了 svc。区别很关键——全局变量是「谁都能从任意角落摸到、还能改」;而 svcCtx.Config 是「只有被传到(handler→logic)的地方才摸得到,且启动后没人改它」。它享受了「全局只读」的便利,却避开了「全局可变」的坑。
svcCtx.Config 是安全的(struct 值拷贝,读字段无锁)。但千万别在运行期写它——要动态配置,请走 Redis / etcd / 配置中心,而不是改这个结构体。核心套路:内嵌框架的 Conf(rest/zrpc)+ 自定义业务字段。框架字段负责端口、超时、日志、JWT;自定义字段负责你的 DB、缓存、第三方密钥。
type Config struct { rest.RestConf // 框架:Name/Host/Port/Mode/Timeout/Verbose/CpuThreshold/Log... // ---- 以下都是你自己的业务配置 ---- Mysql sqlx.MysqlConf // 数据库 CacheRedis redis.RedisConf // 单节点/集群 Redis Auth struct { // JWT 鉴权 AccessSecret string AccessExpire int64 } UserRpc zrpc.RpcClientConf // 内部 RPC 客户端(含 etcd 发现) PayHttp httpc.ServiceConf // 外部 HTTP 客户端 }
rest.RestConf / zrpc.RpcConf,涵盖监听地址、运行模式、超时、CPU 阈值、日志、Prometheus、JWT(Auth 也常在这里)。由 go-zero 框架直接消费。json:"xxx" 标签时以标签为准。内嵌的 RestConf 字段是平铺进 YAML 根级的(Name/Port 直接写顶层)。统一通过 l.svcCtx.Config 读——和读 DB、Redis 一个入口。
// internal/logic/userlogic.go func (l *UserLogic) UserInfo() (*types.UserInfoResp, error) { secret := l.svcCtx.Config.Auth.AccessSecret // 读自定义配置 timeout := l.svcCtx.Config.Timeout // 读框架配置(内嵌) dsn := l.svcCtx.Config.Mysql.DataSource // 读 DB 配置 ... }
读配置 = 读 svc 字段,没有任何「全局函数 config.Get()」。好处是单测时你可以 new 一个带假配置的 ServiceContext 直接注入,无需改环境、无需全局状态。
| 框架 | 配置怎么加载/存 | 是否全局可变 | 和 go-zero 的差异 |
|---|---|---|---|
| go-zero | conf.MustLoad 解析 → 实例传进 svc → svcCtx.Config |
只读、随 svc 走,无全局变量 | 自带解析器、YAML 优先、无热更新,强约束「配置即依赖」 |
| Spring Boot | @ConfigurationProperties / @Value 注入 Bean,Environment 持有 |
Bean 单例、框架内可注入;支持 @RefreshScope 热更新 |
反射 + 注解,配置中心/Profile 生态更丰富,热更新是标配 |
| Gin + viper | viper 多层合并(文件/ENV/flag),自行塞进结构体或全局 | 常写成全局 viper.Get(),易变 |
更灵活(多格式/多源),但「存哪」由你定,go-zero 直接规定存 svc |
| Go 标准库 | flag + os.Getenv 手动解析 |
多为包级变量,最随意 | 无约定,go-zero 给了开箱即用的约定 |
| 你的疑问 | 答案 |
|---|---|
| 配置文件怎么解析的? | 用 core/conf 包(默认 YAML,也支持 JSON),conf.MustLoad(path, &c) 启动时一次性读文件、做环境变量替换、按字段名/标签反序列化进结构体。 |
| 怎么存的? | config 包只定义 type Config struct;main 里解析出局部变量 c,再作为参数传进 svc.NewServiceContext(c)。 |
| 存成 config 模块下的全局变量吗? | 不是。config 包里没有任何包级全局变量,只有类型定义。 |
| 还是存到 svc 中? | 是的(实例层面)。配置实例以 svcCtx.Config 字段的形式住在 svc 里,但它是显式传参进去的,不是隐式全局。 |
| 业务怎么读? | 统一走 l.svcCtx.Config.XXX,启动后只读、并发安全;要动态配置请外接配置中心。 |
终极一句话:go-zero 的 config =「config 包定类型 + main 解析 + svc 收为字段」。它既不是全局变量,也不游离于 svc 之外——而是被当作第一等依赖,和其他连接一起,整齐地排在 ServiceContext 里。