go-zero 真的不用全局变量吗?

「听说 go-zero 不建议用全局变量」—— 是的,但不是「完全不用」。这篇把:建议不用啥、到底还用不用、不用了用啥、以及是不是「啥都靠 svc 解决」一次讲清。

真相 建议不用的是啥 哪些还用 替代方案 svc 能解决一切吗 正反示例 结论

真相:不建议「可变共享状态」型全局变量

go-zero 反对的是把数据库连接、RPC 客户端、缓存、配置这些「有状态的依赖」塞进包级全局变量。普通常量、不可变配置、注册表这类全局,框架自己也在用。

所以准确说法是:go-zero 不建议用全局变量来承载「依赖/连接/可变状态」,而不是「Go 里所有全局变量都被禁止」。把这点分清,后面都好懂。

📌 这套规范和 Go 官方「接收接口、返回结构体」「组合优于继承」「显式依赖」的 idiom 一脉相承,go-zero 只是用 svc 把它强制落地了(详见 svc 注入篇)。

建议不用的是哪些全局变量

一句话:凡是「会被并发读写、有生命周期、需要替换/测试」的依赖,都别放全局。

❌ 全局 DB / Redis 句柄
var DB *gorm.DB,全局可变连接,难测试、难控制初始化顺序。
❌ 全局 RPC / HTTP 客户端
var UserClient user.User,全局单例客户端,测试时无法注入 mock。
❌ 全局可变配置
运行时会被改的 map/struct,多 goroutine 并发改不安全。
❌ 全局缓存 / 计数器
自己维护的 var cache = map[...],并发写要加锁,还绕过了 svc 里的依赖管理。

哪些全局变量其实还在用

别走极端。以下全局是合理甚至必要的,go-zero 自己也用。

✅ 包级常量
const Timeout = 3、错误码、枚举 —— 不可变,安全。
✅ 注册表 / 工厂表
如 middleware 注册、codec 注册,init() 里登记,只读映射。
✅ 无状态工具包
logxjsonx 这类包级函数,本身无可变状态,配置通过参数传入。
✅ 进程级单例(谨慎)
如全局 tracer、全局 logger,通常用 sync.Once 初始化且设计为并发安全。
⚠️ 判断标准:这个全局变量会不会被并发修改、要不要被替换做测试、有没有生命周期。会 → 别放全局;不会(纯只读/无状态)→ 可以。

不用全局变量,那用啥

三类场景,三种归宿:依赖交给 svc,请求数据交给 ctx,共享状态交给「依赖里的并发安全组件」。

依赖 / 连接 / 客户端 DB / RPC / Cache / 配置 请求级数据 userId / traceId / token 共享可变状态 用 svc 里的并发安全组件 → 放 svc.ServiceContext → 放 context.Context → 放 Redis / sync.Map 等
🔧 依赖 → svc
所有连接/客户端在 ServiceContext 里建一次,logic 通过 svcCtx.Xxx 取用。
🔧 请求数据 → ctx
单请求内的 userId、traceId 用 context.Context 携带,绝不塞全局。go-zero 全程传 ctx 正是此意。
🔧 共享缓存 → 组件
需要跨请求共享?用 svc 里的 Redis 客户端或 sync.Map,而不是自己搞个全局 map。
🔧 常量/枚举 → 全局
不可变的,放心用包级 const

「啥都靠 svc 解决」?—— 不是

svc 确实承担了「替代全局依赖」的主力,但它不是万能容器。下面这张表讲清它的边界。

用途svc 能解决吗正确归宿
放 DB / RPC / HTTP 客户端✅ 正是它的本职svc.ServiceContext
放运行时配置(端口、密钥)✅ 配置对象放进去svcCtx.Config
放「单请求」的用户身份/链路ID❌ 不该放context.Context(请求级,svc 是进程级)
放「不可变常量/枚举」❌ 没必要包级 const
放「跨请求共享的可变状态」❌ 不直接放svc 里的 Redis / 并发安全组件

svc 解决的是「进程级、单例、被多处复用的依赖」。 它替代的是「全局变量存依赖」这个反模式;但它不替代 ctx(请求级数据)、不替代 const(常量)、也不替代并发安全组件(真正的共享状态)。所以「真就啥都用 svc」是误解——它是主力,不是全部。

同一个需求,两种写法

需求:logic 里需要查数据库。下面看「全局变量写法」和「go-zero 推荐写法」。

❌ 反模式:全局变量存 DB

// db.go —— 全局可变句柄
var DB *gorm.DB

func init() {
    DB, _ = gorm.Open(...)   // 初始化顺序难控,测试难替换
}

// logic 里直接用全局 DB
func (l *XxxLogic) Do() {
    DB.Where("id=?", 1).Find(&u)  // 隐式依赖,看不出从哪来
}

✅ 推荐:依赖注入进 svc

// svc/servicecontext.go —— 依赖在 main 建一次
type ServiceContext struct {
    Config  config.Config
    DB      *gorm.DB          // 显式声明
}
func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{Config: c, DB: gorm.Open(...)}
}

// logic 里从 svcCtx 取,清晰、可替换、可测试
func (l *XxxLogic) Do() {
    l.svcCtx.DB.Where("id=?", 1).Find(&u)
}
💡 注意:这里 *gorm.DB 本身还是那个连接对象,只是不再藏在全局变量里,而是被显式放进 svc 结构体。go-zero 改的是「存放位置」和「可见性」,不是消灭依赖本身。

结论速记

1. 不建议用全局变量吗?
对「依赖/连接/可变状态」型全局变量,强烈不建议。
2. 全局变量完全不用吗?
不是。常量、注册表、无状态工具、并发安全单例仍在用。
3. 不用了用啥?
依赖→svc;请求数据→ctx;共享状态→并发安全组件;常量→const。
4. 啥都靠 svc 解决吗?
不。svc 只管进程级单例依赖;ctx / const / 并发组件各有各的坑位。
一句话记住:go-zero 用 svc 把「依赖」从全局变量里救出来,但 ctx 管请求、const 管常量、并发组件管共享——别把 svc 当垃圾桶。