「听说 go-zero 不建议用全局变量」—— 是的,但不是「完全不用」。这篇把:建议不用啥、到底还用不用、不用了用啥、以及是不是「啥都靠 svc 解决」一次讲清。
go-zero 反对的是把数据库连接、RPC 客户端、缓存、配置这些「有状态的依赖」塞进包级全局变量。普通常量、不可变配置、注册表这类全局,框架自己也在用。
所以准确说法是:go-zero 不建议用全局变量来承载「依赖/连接/可变状态」,而不是「Go 里所有全局变量都被禁止」。把这点分清,后面都好懂。
svc 把它强制落地了(详见 svc 注入篇)。一句话:凡是「会被并发读写、有生命周期、需要替换/测试」的依赖,都别放全局。
var DB *gorm.DB,全局可变连接,难测试、难控制初始化顺序。var UserClient user.User,全局单例客户端,测试时无法注入 mock。var cache = map[...],并发写要加锁,还绕过了 svc 里的依赖管理。别走极端。以下全局是合理甚至必要的,go-zero 自己也用。
const Timeout = 3、错误码、枚举 —— 不可变,安全。init() 里登记,只读映射。logx、jsonx 这类包级函数,本身无可变状态,配置通过参数传入。sync.Once 初始化且设计为并发安全。三类场景,三种归宿:依赖交给 svc,请求数据交给 ctx,共享状态交给「依赖里的并发安全组件」。
ServiceContext 里建一次,logic 通过 svcCtx.Xxx 取用。context.Context 携带,绝不塞全局。go-zero 全程传 ctx 正是此意。sync.Map,而不是自己搞个全局 map。const。svc 确实承担了「替代全局依赖」的主力,但它不是万能容器。下面这张表讲清它的边界。
| 用途 | svc 能解决吗 | 正确归宿 |
|---|---|---|
| 放 DB / RPC / HTTP 客户端 | ✅ 正是它的本职 | svc.ServiceContext |
| 放运行时配置(端口、密钥) | ✅ 配置对象放进去 | svcCtx.Config |
| 放「单请求」的用户身份/链路ID | ❌ 不该放 | context.Context(请求级,svc 是进程级) |
| 放「不可变常量/枚举」 | ❌ 没必要 | 包级 const |
| 放「跨请求共享的可变状态」 | ❌ 不直接放 | svc 里的 Redis / 并发安全组件 |
svc 解决的是「进程级、单例、被多处复用的依赖」。 它替代的是「全局变量存依赖」这个反模式;但它不替代 ctx(请求级数据)、不替代 const(常量)、也不替代并发安全组件(真正的共享状态)。所以「真就啥都用 svc」是误解——它是主力,不是全部。
需求:logic 里需要查数据库。下面看「全局变量写法」和「go-zero 推荐写法」。
// db.go —— 全局可变句柄 var DB *gorm.DB func init() { DB, _ = gorm.Open(...) // 初始化顺序难控,测试难替换 } // logic 里直接用全局 DB func (l *XxxLogic) Do() { DB.Where("id=?", 1).Find(&u) // 隐式依赖,看不出从哪来 }
// 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 改的是「存放位置」和「可见性」,不是消灭依赖本身。