go-zero 的初始化写在哪?

「一堆数据库连接、RPC 接口链接、Redis……初始化代码到底写到哪里了?」—— 这篇把 go-zero 的启动序列、初始化代码的归属(main.gosvc.ServiceContext)、以及各类依赖怎么建一次讲清。

一句话结论 初始化指什么 代码写到哪了 svc 构造函数示例 启动顺序图 为什么集中到 svc 常见依赖怎么建 其他框架 结论

初始化代码集中在一个地方:svc.NewServiceContext

go-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 里。

在 go-zero 里,「初始化」到底指什么

不是给某个变量赋零值,而是「进程启动时、只跑一次、给整个程序准备好共享资源」。

🧩 属于初始化的动作
建立数据库连接池(GORM / sqlx)、连 Redis、创建下游 RPC/HTTP 客户端、连消息队列、读配置、建缓存实例、注册中间件等。特点:建一次、所有请求复用。
🚫 不属于初始化的动作
处理单次请求的业务逻辑(在 logic)、从 ctx 取 userId / traceId(请求级数据,走 context)、纯函数计算。这些随请求而来,不是启动期一次性动作。
关键区分:「进程级共享依赖」(建一次复用)才需要初始化,放进 svc;「请求级数据」(每个请求不同)不初始化,走 context.Context。详见「svc 深度解析」那篇。

初始化代码,都写到哪了?

只分布在两个文件,且职责清晰分开:main.go 负责「编排」,svc/servicecontext.go 负责「具体建连接」。

user/ ├── etc/ │ └── user.yaml ← 配置文件(连接串/地址) ├── internal/ │ ├── config/ │ │ └── config.go ← 只定义 struct,无全局变量 │ ├── svc/ │ │ └── servicecontext.go ★ 初始化大本营:建 DB/Redis/RPC/HTTP │ ├── handler/ ... │ ├── logic/ ... │ └── types/ ... └── user.go ← main 入口(goctl 生成的名字是 xxx.go)
📍 main.go —— 编排三件事
conf.MustLoad 解析配置;② svc.NewServiceContext(c) 把全部依赖建好;③ rest.MustNewServer / zrpc.MustNewServer 起服务并把 svc 传进去。它自己几乎不写建连接代码,只负责串联。
📍 svc/servicecontext.go —— 真正建连接
结构体 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.DBl.svcCtx.UserRpc 就能用,连接已经是好的。这就是 go-zero「依赖集中在 composition root 一次性装配」的落地形态。

进程启动时的完整顺序

从敲下 go run 到开始接请求,初始化只发生在最前面的一段。

① conf.MustLoad 解析 etc/*.yaml ② svc.NewServiceContext 建 DB/Redis/RPC/HTTP ③ MustNewServer rest/zrpc 起服务 ④ Start() 接请求 初始化只发生在 ① + ②;③ 起监听但不重建连接;④ 之后每个请求复用 svc 里的连接 所以「建连接」这件事,整个进程生命周期里只跑一次
// 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()                               // ④ 开始接请求
}

为什么 go-zero 把初始化集中到 svc

这背后是「组合根(Composition Root)」这一经典设计,go-zero 用代码生成把它变成强制约定。

🎯 单一装配点
所有连接只在 NewServiceContext 里建,想看「程序依赖了哪些外部资源」打开这一个函数就够,不会散落十几个文件。
🔁 只建一次
连接池、RPC 客户端都是昂贵的资源。集中建一次、指针传给所有 handler/logic,避免重复建连、连接泄漏。
🧪 易测试
测试时手动 new 一个 ServiceContext,把 DB/RPC 换成 mock 字段即可,不用改 logic、不用全局变量。
🔒 类型安全 + 生成一致性
字段都在 struct 上,编译期就能发现漏配;goctl 生成的骨架保证每个服务都长一样,新人一眼看懂。

常见依赖,在 go-zero 里怎么初始化

都在 svc.NewServiceContext 里,按「建一次 + 存字段」的套路。

依赖类型创建方式(写在 svc 里)字段类型说明
关系型 DB(MySQL)sqlx.NewMysql(c.Mysql.DataSource)sqlx.SqlConn连接池,model 层用
Redisredis.New(c.CacheRedis)*redis.Redis缓存 / 限流 / 锁
下游 RPC(zrpc)user.NewUser(zrpc.MustNewClient(c.UserRpc))user.User向 etcd 发现、建客户端池
外部 HTTPhttpc.NewService(c.PayHttp)httpc.Service自带超时 + 熔断
消息队列kq.NewQueue(c.KqConf)具体 MQ 类型如 Kafka 消费队列
业务聚合 serviceNewOrderService(svc)自定义 struct把多个依赖打包成业务组件
踩坑提醒:① 别在 NewServiceContext 里写业务逻辑,它只负责「建」;② 需要连接失败的快速失败就用 MustNewXxx(启动即崩,比半死不活强);③ 依赖之间有顺序时,在构造函数里按字段顺序排好,go-zero 不会帮你排序。

其他框架也这么做吗?都一样吗?

「启动时集中装配依赖」是 Go 圈的共识,但机制不同。

🔹 裸 net/http / 小项目
常在 main() 里顺序建 sql.Openredis.NewClient,再塞进 handler 结构。思路一样(集中建一次),但没有 svc 这一层强制约定,容易退化为全局变量。
🔹 Gin / Echo
多把依赖挂到 engine 或自定义的 App 结构体上(app.DB),main 里装配。和 go-zero 的 svc 形态最接近,但同样靠自觉,不强制。
🔸 google/wire
用注解 + 生成器在编译期算出依赖装配代码(InitializeApp()),哲学和 go-zero 的 svc 几乎一致:composition root + 生成。区别:wire 是通用 DI 生成器,go-zero 把 svc 直接写进分层骨架。
🔸 uber/dig + fx
运行时反射做依赖注入容器,fx.Provide 注册、fx.Invoke 装配。go-zero 刻意避开运行时反射,走「编译期确定 + 纯 struct」,更可控、零魔法。
结论:「初始化集中在启动时一次性装配」是全 Go 圈共识;go-zero 的标签是 svc 这一层 + goctl 强制生成 + 无反射 + 绑定五层分层。所以不是「谁都长一样」,但目标一致——把昂贵资源建一次、可控、可测。

一句话记住 go-zero 的初始化

🧠 记忆口诀

· 初始化代码不在 init()不在 logic不在全局变量;
· svc/servicecontext.goNewServiceContext 里建一次;
· 由 main.go 编排三步走(解析配置 → 建 svc → 起服务);
· DB / Redis / RPC / HTTP / MQ 全部「建一次、存字段、指针传下去」;
· 之后每个请求的 logic 通过 l.svcCtx.XXX 直接复用,连接已经就绪。