go-zero 的 svc 注入,到底是什么?

你一定在 go-zero 项目里见过 svc.ServiceContext 这种东西:main 里建一次,handler/logic 里到处用。它是什么技术?规范从哪来?为了解决啥?别的 Go 框架也这么干吗?

它是什么 什么技术 规范哪来的 解决什么问题 别的框架怎么干 都一样吗

它长什么样

svc 注入 = 一个叫 ServiceContext 的结构体,把所有外部依赖(DB、RPC 客户端、缓存、配置)集中装进去,再一路传给每个 logic。

main 加载配置 ServiceContext DB / RPC / Cache Config handler A 注册路由 handler B 注册路由 handler C 注册路由 logic svcCtx.*

典型代码(go-zero 生成的结构)

// internal/svc/servicecontext.go
type ServiceContext struct {
    Config    config.Config
    UserModel model.UserModel      // DB 模型
    UserRpc   user.User            // 下游 RPC 客户端
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:    c,
        UserModel: model.NewUserModel(sqlx.NewMysql(c.Mysql.DataSource), c.CacheRedis),
        UserRpc:   user.NewUser(zrpc.MustNewClient(c.UserRpc)),
    }
}

logic 构造函数接收它,业务里直接 l.svcCtx.UserModel 取用:

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
    return &LoginLogic{ctx: ctx, svcCtx: svcCtx}
}
func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) {
    u, _ := l.svcCtx.UserModel.FindOne(l.ctx, 1)  // 直接用
    _ = u
    return &types.LoginResp{}, nil
}

一句话:svc 注入就是把「依赖装配」这件事集中到一处,做成一个结构体,再依赖它往下游传。它本身不是 go-zero 发明的新技术,而是一种依赖注入(DI)的落地方式。

它到底是什么技术

剥掉 go-zero 的命名外壳,它背后是三个软件工程老概念的组合。

① 依赖注入(DI)
把「创建依赖」和「使用依赖」分开。logic 不自己 new DB,而是由外部把 DB 塞进来。好处是解耦 + 可测试(注入 mock 即可)。
② 组合根(Composition Root)
所有依赖在程序「最外层」(main → svc)一次性组装好,业务代码不关心怎么造出来的。这是 DI 的经典落点。
③ 结构体当容器
不用接口、不用泛型、不用反射,就用一个普通 struct 当「依赖容器」——这是 Go 社区最朴素的 DI 手法。
④ 代码生成驱动
关键差异点:这个 struct 和构造函数是 goctl 自动生成的,不是你手写的,所以「规范」能稳定落地。
不是 Java Spring 那种「容器 + 注解 + 反射」的 IoC,也不是运行时依赖查找。它是编译期确定、纯结构体、无反射的手动 DI。这正是 Go 语言「显式优于隐式」哲学的体现。

这个规范从哪来

它有两个来源:上层的工程哲学,和下层的工具强制。

1)Go 社区的通用 idiom

Go 官方和社区一直倡导:「接收接口、返回结构体」组合优于继承依赖在顶层装配后向下传。ServiceContext 只是把这个 idiom 用「一个共享 struct」的形式固定下来。

2)go-zero 的分层约定 + goctl 强制

go-zero 官方把服务固定为 handler / logic / svc / types / config 五层。其中 svc 专门负责「装配」,logic 只消费。goctl 生成代码时直接产出 NewServiceContext 和接收它的 logic 构造函数,开发者照着填空即可,规范由工具保证一致

3)从「全局变量」踩坑里长出来的

很多新手本能地写 var db *sql.DB 全局变量,结果测试难 mock、初始化顺序混乱、并发不安全。ServiceContext 把依赖收进一个显式传递的对象,正是为了治这个毛病。规范本质上是「前人踩坑后的最佳实践沉淀」。

它解决了什么问题

消灭全局变量
依赖随 struct 显式流动,没有隐式状态,初始化顺序清晰可控。
易于测试
logic 只认 ServiceContext 字段,单测时把 DB/RPC 换成 mock 实现即可,不用改业务代码。
单一装配点
所有连接(MySQL、Redis、RPC)只在 svc 里建一次,避免重复建连、资源泄漏。
解耦与可替换
logic 依赖的是 struct 上的字段/接口,底层实现想换(如 DB 换驱动)只改 svc 一处。
类型安全
纯 Go 结构体,编译期就能发现「忘了注入某依赖」,不用等运行时 panic。
生成一致性
goctl 保证每个服务结构一致,团队新人看一个就懂全部,降低协作成本。

别的 Go 框架怎么干

结论先说:「依赖要注入、不要全局变量」是全 Go 社区的共识,但「用什么机制注入」差别很大。下面看几类典型做法。

① 标准库 net/http:Handler 即依赖容器(最经典)

最常见的「Go 原生」写法——把依赖塞进 Handler 结构体,方法当 handler。本质上和 ServiceContext 是同一个思想(组合),区别在于它是「每 handler 一份」而非「全局一份」

type UserHandler struct {
    db      *sql.DB
    userSvc *UserService
}
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
    u, _ := h.userSvc.Login(r.Context(), ...)
    _ = u
}
func main() {
    db := mustOpenDB()
    h := &UserHandler{db: db, userSvc: NewUserService(db)}  // 顶层装配
    http.HandleFunc("/login", h.Login)
}

② Gin / Echo 等:Handler 结构 + Context 传值

Web 框架大多沿用「handler 结构装依赖」;另外提供 c.Set / c.Get单次请求内传递值(请求级,不是应用级)。应用级依赖还是靠结构体注入。

// Gin:依赖放 controller 结构
type UserCtrl struct{ svc *UserService }
func (c *UserCtrl) Login(ctx *gin.Context) { ... }

// 请求级临时值用 context
ctx.Set("traceId", "abc")
v, _ := ctx.Get("traceId")

③ google/wire:编译期生成装配代码

和 go-zero 哲学最接近——都是「生成 wiring 代码、不用反射」。你声明 provider,wire 生成 InitializeXxx() 把依赖串起来。区别是 wire 是通用 DI 工具,go-zero 把它焊死在自己的分层里。

func InitializeUserServer() (*UserServer, error) {
    wire.Build(NewDB, NewUserService, NewUserServer)  // 声明式
    return nil, nil  // wire 会生成真正实现
}

④ uber/dig + fx:运行时反射 IoC 容器

另一派:用反射在运行时按类型自动解析依赖,配合 fx 做全生命周期管理。能力强但「有魔法」——调试时不易看清依赖从哪来。go-zero 刻意走这条路,为的是显式、可调试。

fx.Provide(NewDB, NewUserService)   // 反射按类型注入
fx.Invoke(func(s *UserService) { ... })
方案机制反射是否生成代码与 go-zero 关系
go-zero svc共享 struct + 构造函数❌ 无✅ goctl 生成本文主角
net/http handler 结构每 handler 一个 struct❌ 无❌ 手写同一思想,粒度更细
Gin/Echohandler 结构 + 请求级 context❌ 无❌ 手写同思想 + 请求级传值
google/wire编译期生成 wiring❌ 无✅ wire 生成哲学最近(显式+生成)
uber/dig + fx运行时反射 IoC 容器✅ 有❌ 运行时go-zero 明确避开的一派

所以,都一样吗?

目标一样,手段不同。「依赖要注入、别用全局变量、在顶层装配」是 Go 圈的共识,几乎所有框架都遵守。但 go-zero 的 svc 注入有三个独特标签

① 共享而非分散
它不是「每 handler 一个 struct」,而是一个全局共享的 ServiceContext 传给所有 logic,集中管理一目了然。
② 工具生成而非手写
NewServiceContext 与构造函数由 goctl 产出,规范强制统一,不依赖个人自觉。
③ 显式无反射
纯 struct、编译期确定、可调试,刻意与 dig/fx 那种「魔法容器」划清界限。
④ 绑定分层
svc 是 go-zero 五层架构(handler/logic/svc/types/config)的有机一环,离开这套分层它就没有意义。
一句话记忆:go-zero 的 svc 注入 = 「编译期、无反射、靠代码生成、共享 struct」 的依赖注入。别的框架大多认同同一目标,但要么手写 handler 结构(net/http、Gin)、要么用生成器(wire)、要么用反射容器(dig/fx)。没有谁「都长一样」,但都在解决同一件事。