你一定在 go-zero 项目里见过 svc.ServiceContext 这种东西:main 里建一次,handler/logic 里到处用。它是什么技术?规范从哪来?为了解决啥?别的 Go 框架也这么干吗?
svc 注入 = 一个叫 ServiceContext 的结构体,把所有外部依赖(DB、RPC 客户端、缓存、配置)集中装进去,再一路传给每个 logic。
// 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 的命名外壳,它背后是三个软件工程老概念的组合。
它有两个来源:上层的工程哲学,和下层的工具强制。
Go 官方和社区一直倡导:「接收接口、返回结构体」、组合优于继承、依赖在顶层装配后向下传。ServiceContext 只是把这个 idiom 用「一个共享 struct」的形式固定下来。
go-zero 官方把服务固定为 handler / logic / svc / types / config 五层。其中 svc 专门负责「装配」,logic 只消费。goctl 生成代码时直接产出 NewServiceContext 和接收它的 logic 构造函数,开发者照着填空即可,规范由工具保证一致。
很多新手本能地写 var db *sql.DB 全局变量,结果测试难 mock、初始化顺序混乱、并发不安全。ServiceContext 把依赖收进一个显式传递的对象,正是为了治这个毛病。规范本质上是「前人踩坑后的最佳实践沉淀」。
结论先说:「依赖要注入、不要全局变量」是全 Go 社区的共识,但「用什么机制注入」差别很大。下面看几类典型做法。
最常见的「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) }
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")
和 go-zero 哲学最接近——都是「生成 wiring 代码、不用反射」。你声明 provider,wire 生成 InitializeXxx() 把依赖串起来。区别是 wire 是通用 DI 工具,go-zero 把它焊死在自己的分层里。
func InitializeUserServer() (*UserServer, error) { wire.Build(NewDB, NewUserService, NewUserServer) // 声明式 return nil, nil // wire 会生成真正实现 }
另一派:用反射在运行时按类型自动解析依赖,配合 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/Echo | handler 结构 + 请求级 context | ❌ 无 | ❌ 手写 | 同思想 + 请求级传值 |
| google/wire | 编译期生成 wiring | ❌ 无 | ✅ wire 生成 | 哲学最近(显式+生成) |
| uber/dig + fx | 运行时反射 IoC 容器 | ✅ 有 | ❌ 运行时 | go-zero 明确避开的一派 |
目标一样,手段不同。「依赖要注入、别用全局变量、在顶层装配」是 Go 圈的共识,几乎所有框架都遵守。但 go-zero 的 svc 注入有三个独特标签: