「一堆数据库连接、RPC 接口链接、Redis……初始化代码到底写到哪里了?」—— 这篇把 go-zero 的启动序列、初始化代码的归属(main.go 与 svc.ServiceContext)、以及各类依赖怎么建一次讲清。
svc.NewServiceContextgo-zero 里所有「进程启动时要建一次的依赖」(DB / Redis / RPC / HTTP 客户端 / MQ 等),都不散落在 init()、logic 或全局变量里,而是统一在 internal/svc/servicecontext.go 的构造函数里创建,由 main.go 调用一次。
记得住的一句话:初始化 = main.go 三步走,第一步解析配置,第二步 svc.NewServiceContext(c) 把 DB/RPC/Redis 全建好,第三步起服务。你写的「建连接」代码,99% 都在 servicecontext.go 里。
不是给某个变量赋零值,而是「进程启动时、只跑一次、给整个程序准备好共享资源」。
logic)、从 ctx 取 userId / traceId(请求级数据,走 context)、纯函数计算。这些随请求而来,不是启动期一次性动作。context.Context。详见「svc 深度解析」那篇。
只分布在两个文件,且职责清晰分开:main.go 负责「编排」,svc/servicecontext.go 负责「具体建连接」。
conf.MustLoad 解析配置;② svc.NewServiceContext(c) 把全部依赖建好;③ rest.MustNewServer / zrpc.MustNewServer 起服务并把 svc 传进去。它自己几乎不写建连接代码,只负责串联。ServiceContext 上挂所有字段(Config / DB / Redis / RpcClient...),构造函数里用 NewXxx(...) 把每个连接建出来并赋值。这是你写初始化代码唯一的主战场。init() 函数(go-zero 不靠它做依赖装配,易顺序错、难测试);② logic 里现建连接(每个请求都建,连接泄漏);③ 全局 var DB(丢失注入、难替换)。
svc.NewServiceContext 长什么样一个真实的「初始化大本营」:配置解析进来,DB、Redis、下游 RPC、外部 HTTP、MQ 全部在此建一次。
package svc import ( "github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/zrpc" "github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/httpc" "your_project/user/internal/config" "your_project/user/rpc/user/user" // 下游 RPC 生成包 ) type ServiceContext struct { Config config.Config DB sqlx.SqlConn // 数据库连接池 Redis *redis.Redis UserRpc user.User // 下游 RPC 客户端 PayHttp httpc.Service // 外部 HTTP 客户端 } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, // ① 数据库:从配置 Mysql 字段建连接池(一次) DB: sqlx.NewMysql(c.Mysql.DataSource), // ② Redis:从配置建连接 Redis: redis.New(c.CacheRedis), // ③ 下游 RPC 客户端:自动向 etcd 发现、建连接池 UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)), // ④ 外部 HTTP 客户端:内置超时+熔断 PayHttp: httpc.NewService(c.PayHttp), } }
NewXxx 都是「建一次、存到字段」。之后 logic 里只要 l.svcCtx.DB、l.svcCtx.UserRpc 就能用,连接已经是好的。这就是 go-zero「依赖集中在 composition root 一次性装配」的落地形态。
从敲下 go run 到开始接请求,初始化只发生在最前面的一段。
// user.go (goctl 生成的 main 入口) func main() { var c config.Config conf.MustLoad("etc/user.yaml", &c) // ① 解析配置 svc := svc.NewServiceContext(c) // ② 初始化:建全部依赖 ctx := svccontext.NewServerContext(context.Background(), c) // 框架上下文 server := rest.MustNewServer(c.RestConf, rest.WithCustomCors(...)) defer server.Stop() handler.RegisterHandlers(server, svc) // 路由注册(不建连接) server.Start() // ④ 开始接请求 }
这背后是「组合根(Composition Root)」这一经典设计,go-zero 用代码生成把它变成强制约定。
NewServiceContext 里建,想看「程序依赖了哪些外部资源」打开这一个函数就够,不会散落十几个文件。ServiceContext,把 DB/RPC 换成 mock 字段即可,不用改 logic、不用全局变量。都在 svc.NewServiceContext 里,按「建一次 + 存字段」的套路。
| 依赖类型 | 创建方式(写在 svc 里) | 字段类型 | 说明 |
|---|---|---|---|
| 关系型 DB(MySQL) | sqlx.NewMysql(c.Mysql.DataSource) | sqlx.SqlConn | 连接池,model 层用 |
| Redis | redis.New(c.CacheRedis) | *redis.Redis | 缓存 / 限流 / 锁 |
| 下游 RPC(zrpc) | user.NewUser(zrpc.MustNewClient(c.UserRpc)) | user.User | 向 etcd 发现、建客户端池 |
| 外部 HTTP | httpc.NewService(c.PayHttp) | httpc.Service | 自带超时 + 熔断 |
| 消息队列 | kq.NewQueue(c.KqConf) 等 | 具体 MQ 类型 | 如 Kafka 消费队列 |
| 业务聚合 service | NewOrderService(svc) | 自定义 struct | 把多个依赖打包成业务组件 |
NewServiceContext 里写业务逻辑,它只负责「建」;② 需要连接失败的快速失败就用 MustNewXxx(启动即崩,比半死不活强);③ 依赖之间有顺序时,在构造函数里按字段顺序排好,go-zero 不会帮你排序。
「启动时集中装配依赖」是 Go 圈的共识,但机制不同。
main() 里顺序建 sql.Open、redis.NewClient,再塞进 handler 结构。思路一样(集中建一次),但没有 svc 这一层强制约定,容易退化为全局变量。engine 或自定义的 App 结构体上(app.DB),main 里装配。和 go-zero 的 svc 形态最接近,但同样靠自觉,不强制。InitializeApp()),哲学和 go-zero 的 svc 几乎一致:composition root + 生成。区别:wire 是通用 DI 生成器,go-zero 把 svc 直接写进分层骨架。fx.Provide 注册、fx.Invoke 装配。go-zero 刻意避开运行时反射,走「编译期确定 + 纯 struct」,更可控、零魔法。
· 初始化代码不在 init()、不在 logic、不在全局变量;
· 在 svc/servicecontext.go 的 NewServiceContext 里建一次;
· 由 main.go 编排三步走(解析配置 → 建 svc → 起服务);
· DB / Redis / RPC / HTTP / MQ 全部「建一次、存字段、指针传下去」;
· 之后每个请求的 logic 通过 l.svcCtx.XXX 直接复用,连接已经就绪。