fx.New() 启动后立刻退出不是 bug,是因为未注册实现 fx.Lifecycle 的长期运行组件,容器执行完 fx.Invoke 后即返回,不阻塞主线程。

fx.New() 启动后立刻退出,不是 bug,是你没注册长期运行组件。
为什么 fx.New().Run() 一执行就结束?
这是新手踩得最多、也最困惑的问题:代码跑完没报错,但进程秒退,HTTP server 没起来,日志只打到 [Fx] RUNNING 就停了。
根本原因在于:fx.New() 构建容器后,默认只执行 fx.Invoke 函数(即“一次性初始化逻辑”),然后就返回 —— 它不阻塞主线程,也不等待任何后台服务。
- 如果你没注册任何实现了
fx.Lifecycle的长期运行组件(比如 HTTP server、gRPC server、消息消费者),程序自然退出 -
fx.Invoke不是“启动服务”的地方,它只适合做 DB 迁移、缓存预热这类单次动作 - 临时调试可加
fx.SleepFor(10 * time.Second),但上线必须用fx.Lifecycle绑定服务生命周期
fx.Provide 必须传构造函数,不能传实例
常见 panic 报错:no constructor found for *sql.DB 或 panic: no constructors provided,基本都源于此。
立即学习“go语言免费学习笔记(深入)”;
fx.Provide 的作用是声明“如何创建某个类型”,而不是“把现成的对象塞进去”。FX 依赖反射分析函数签名来推导依赖链,传值会直接断掉这个过程。
- ✅ 正确写法:
fx.Provide(func(cfg config.Config) (*sql.DB, error) { return sql.Open("mysql", cfg.DSN) }) - ❌ 错误写法:
fx.Provide(db)、fx.Provide(logrus.New())、fx.Provide(&MyService{}) - 函数必须可导出(首字母大写),返回值最多两个:主对象 + 可选
error - 参数类型必须已在容器中注册过,否则启动时找不到依赖,直接 panic
自定义 logger 必须实现 fxevent.Logger,不是 fx.Logger
fx.WithLogger 看似灵活,实则接口约束极强。传错类型不会警告,而是启动即崩,错误信息常为 cannot assign ... to fxevent.Logger。
Uber FX 内部日志系统基于 fxevent.Logger 接口(在 go.uber.org/fx/fxevent 中),不是你熟悉的 log.Logger、zap.Logger 或 zerolog.Logger。
- ✅ 正确路径:自己包装,实现
fxevent.Logger方法(LogEvent是核心);或直接用fx.ZapLogger(它已做好适配) - ❌ 错误写法:
fx.WithLogger(func() *log.Logger { return log.New(...) })→ 类型不匹配,panic - 别用
fx.NopLogger测试“是否生效”——它真的一句不打,不是“静默”,是彻底空转 - 如果用了
fx.ZapLogger却看不到 module 字段或 trace,说明没传fx.WithLogger,而是误用了fx.Provide(zap.New(...))
fx.Invoke 函数体里必须实际使用所有参数
FX 不是简单按类型注入后调用,它会在启动时静态检查:函数签名里的每个参数,是否在函数体内被真正引用。漏用一个,就拒绝启动。
这容易发生在日志、监控等“辅助参数”上 —— 你加了 *log.Logger 参数想打点日志,但忘了在函数体里调用 logger.Print,FX 就认为这个依赖是冗余的,直接报错。
- 函数签名必须严格匹配:参数类型、顺序、数量都不能多也不能少
- 哪怕类型对得上,只要函数体里没出现该变量名,FX 就判定为“未使用”,启动失败
- 需要注入接口但又不想在函数体里用?用
fx.Annotated显式标注,或改用fx.Supply提供固定值(但注意它不参与生命周期) - 构造函数里不要做耗时操作(如连 DB、发 HTTP 请求)——这些同步阻塞会卡住整个启动流程,应移到
fx.OnStart钩子里
真正难的不是写对语法,而是理解 FX 的隐含契约:依赖图在 fx.New() 时就冻结,生命周期阶段不可逆,OnStart 执行时机早于任何非 lifecycle 依赖的首次使用。很多“看似合理”的代码,败就败在违反了其中一条。


















