冷启动时依赖初始化不能全塞进main函数,因为Go在main执行前已完成init()和全局变量初始化,该阶段无法并发、不可控序、无超时机制;而全挪入main易导致串行阻塞,首请求仍需等待全部完成。

为什么冷启动时依赖初始化不能全塞进 main 函数
main 函数执行前,Go 已完成所有 init() 和全局变量初始化,这部分逻辑无法并发、无法控制顺序、无法超时退出——比如 var db = sql.Open(...) 或 var cfg = loadFromS3() 会直接卡住冷启动。而把所有初始化挪到 main 里又容易写成串行阻塞调用,首请求仍要等全部就绪。
用 errgroup 并发初始化可独立组件
真正能并发且带错误聚合的,是 errgroup.Group。它比裸写 goroutine + sync.WaitGroup 更安全:自动传播第一个 panic/err,支持上下文取消。
- 每个依赖(DB、Redis、gRPC client、配置监听器)封装成独立的初始化函数,返回 error
- 在 main 中用
eg.Go(func() error { return initDB() })并发启动 - 调用
eg.Wait()阻塞直到全部完成或任一失败 - 注意:有强依赖的组件(如 Logger 必须早于所有其他日志记录)仍需串行前置,不能并行
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var eg errgroup.Group
eg.SetLimit(3) // 控制并发数,避免打爆下游
eg.Go(func() error { return initLogger() })
eg.Go(func() error { return initConfig() })
eg.Go(func() error { return initDB() })
eg.Go(func() error { return initCache() })
if err := eg.Wait(); err != nil {
log.Fatal("init failed: ", err)
}
}
哪些依赖适合并行,哪些必须串行
并行的前提是“无共享状态、无顺序依赖”。常见误判点:
-
initDB()和initCache()可并行——它们各自建连接,互不干扰 -
initMetrics()依赖initConfig()里的上报地址,必须等 config 完成后再启动 -
http.ServeMux注册 handler 前,必须确保所有 service 实例已构造完毕(因 handler 闭包捕获 service 指针) - 任何用到
sync.Once的懒加载资源(如全局 HTTP client),其首次调用时机不可控,不适合放在并行组里——应保留在 handler 内部按需触发
健康检查如何反映真实就绪状态
并发初始化完成后,服务可能仍处于“逻辑未就绪”状态:比如 DB 连上了但 migration 没跑完,或者 cache 连上了但预热数据没加载。这时 /healthz 返回 200 是误导。
立即学习“go语言免费学习笔记(深入)”;
- 定义
type HealthChecker interface { Check(ctx context.Context) error } - 每个关键组件实现该接口(DB ping、cache ping、migration status)
- /readyz 路由聚合所有
Check()结果,任一失败返回 503 - 避免在 /readyz 里做新连接——复用初始化阶段已建立的 client 实例
最易忽略的是:并发初始化成功 ≠ 服务可对外提供业务逻辑。Ready 状态必须包含业务层校验,比如“用户服务是否已加载默认角色配置”,这往往需要额外字段和初始化后的一次同步校验。


















