
golang-uber-fx
热门使用 uber-go/fx 的 Golang 应用框架——包括 fx.New、fx.Provide、fx.Invoke、fx.Module、fx.Lifecycle 钩子、fx.Annotate(name/group/As)、fx.Decorate、fx.Supply、fx.Replace、fx.WithLogger 以及信号感知的 Run()。适用于使用或采用 uber-go/fx 的场景,当代码库导入 `go.uber.org/fx` 或使用 fx.New 编排服务时。对于无生命周期的原始 DI,请参见 `samber/cc-skills-golang@golang-uber-dig` 技能。
使用 uber-go/fx 的 Golang 应用框架——包括 fx.New、fx.Provide、fx.Invoke、fx.Module、fx.Lifecycle 钩子、fx.Annotate(name/group/As)、fx.Decorate、fx.Supply、fx.Replace、fx.WithLogger 以及信号感知的 Run()。适用于使用或采用 uber-go/fx 的场景,当代码库导入 `go.uber.org/fx` 或使用 fx.New 编排服务时。对于无生命周期的原始 DI,请参见 `samber/cc-skills-golang@golang-uber-dig` 技能。
角色: 你是一位使用 fx 构建长期运行服务的 Go 架构师。你在组合根处编排依赖图,将生命周期逻辑推入钩子而非 init(),并将模块视为可复用的单元。
在 Go 中使用 uber-go/fx 进行应用编排
应用框架,结合了基于反射的 DI 容器(构建于 uber-go/dig 之上)、生命周期、模块系统、信号感知的运行循环和结构化事件日志。适用于启动顺序、优雅关闭和模块化组合至关重要的长期运行服务。
官方资源:
本技能并非详尽无遗。请参考库文档和代码示例以获取更多信息。Context7 可作为可发现性平台提供帮助。对于 Go 包文档、版本、符号和已知漏洞,→ 参见 samber/cc-skills-golang@golang-pkg-go-dev 技能。
go get go.uber.org/fx
fx 与 dig 对比
fx 构建于 dig 之上,共享相同的基于反射的容器引擎。DI 原语(Provide、Invoke、In/Out 结构体、命名值、值组)是相同的——fx.In/fx.Out 是 dig.In/dig.Out 的重新导出。
fx 在此基础上增加的内容:
| 关注点 | dig | fx |
|---|---|---|
| DI 容器 | ✅ dig.New() |
✅(内嵌) |
| 生命周期钩子 | ❌ | ✅ fx.Lifecycle OnStart/OnStop |
| 模块系统 | ❌ | ✅ fx.Module 带作用域装饰器 |
| 信号感知运行循环 | ❌ | ✅ app.Run() 阻塞等待 SIGINT/SIGTERM |
| 结构化事件日志 | ❌ | ✅ fx.WithLogger / fxevent |
| 启动/关闭超时 | ❌ | ✅ fx.StartTimeout / fx.StopTimeout |
选择 fx 用于长期运行的服务(HTTP 服务器、工作进程、守护进程)——生命周期和信号处理是必需的,模块使大型服务图易于管理。
选择原始 dig 当你需要无框架的编排时:CLI 工具、向调用者暴露容器的库、测试工具,或将 DI 嵌入到管理自身生命周期的现有应用中。参见 samber/cc-skills-golang@golang-uber-dig 技能。
应用
import "go.uber.org/fx"
app := fx.New(
fx.Provide(NewLogger, NewDatabase, NewServer),
fx.Invoke(RegisterRoutes),
)
app.Run() // 阻塞直到 SIGINT/SIGTERM,然后运行 OnStop 钩子
启动阶段:fx.New 验证类型(构造函数不运行);app.Start(ctx) 运行每个 fx.Invoke 并按拓扑顺序触发 OnStart 钩子;main 阻塞在 app.Done() 上;app.Stop(ctx) 按逆序触发 OnStop 钩子。默认超时为 15 秒——使用 fx.StartTimeout / fx.StopTimeout 覆盖。
Provide 和 Invoke
fx.New(
fx.Provide(NewLogger, NewDatabase, NewServer), // 惰性
fx.Invoke(RegisterRoutes, StartMetricsExporter), // 在 Start 期间始终运行
)
fx.Provide 注册构造函数;fx.Invoke 是触发器——如果没有 Invoke(直接或间接)引用某个类型,其构造函数永远不会运行。
生命周期钩子
注入 fx.Lifecycle 并追加钩子。构造函数应快速返回;长时间运行的工作属于 OnStart。
func NewHTTPServer(lc fx.Lifecycle, log *zap.Logger, cfg *Config) *http.Server {
srv := &http.Server{Addr: cfg.Addr}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil { return err }
go srv.Serve(ln) // 在 goroutine 中执行阻塞工作
return nil
},
OnStop: func(ctx context.Context) error {
return srv.Shutdown(ctx)
},
})
return srv
}
两个回调都接收一个由 StartTimeout/StopTimeout 限定的上下文——尊重取消。OnStart 必须快速返回——为阻塞工作启动 goroutine;否则启动会挂起,依赖的钩子永远不会触发。
fx.StartHook / fx.StopHook / fx.StartStopHook 适配更简单的签名(无上下文、无错误,或两者都有):
lc.Append(fx.StartStopHook(srv.Start, srv.Stop)) // 匹配对
参数和结果对象
fx 重新导出 dig 的 dig.In / dig.Out 作为 fx.In / fx.Out。当构造函数有 4 个以上依赖项,或需要 name/group/optional 标签时使用它们。
type ServerParams struct {
fx.In
Logger *zap.Logger
DB *sql.DB
Cache *redis.Client `optional:"true"`
Routes []http.Handler `group:"routes"`
}
func NewServer(p ServerParams) *Server { /* ... */ }
fx.Annotate
fx.Annotate 包装构造函数以添加标签或接口绑定,无需 fx.Out 结构体。优先使用它来获得 ergonomic 的 name/group/As 绑定:
fx.Provide(
fx.Annotate(NewPrimaryDB, fx.ResultTags(`name:"primary"`)),
fx.Annotate(NewPostgresDB, fx.As(new(Database))), // 暴露接口
fx.Annotate(NewUserHandler,
fx.As(new(http.Handler)),
fx.ResultTags(`group:"routes"`),
),
)
值组
多个构造函数,一个消费者切片——典型用于路由、健康检查、指标收集器:
type RouteResult struct {
fx.Out
Handler http.Handler `group:"routes"`
}
type ServerParams struct {
fx.In
Routes []http.Handler `group:"routes"`
}
追加 ,flatten(group:"routes,flatten")以展开切片而非嵌套。顺序不保证——当顺序重要时,提供一个显式的有序切片。
fx.Module
fx.Module 将提供者、调用和装饰器分组到一个名称下。模块将装饰器限定在自身及其子模块内——在 fx.Module("db", ...) 中重命名的日志记录器仅在该模块内的代码中生效。
var DatabaseModule = fx.Module("database",
fx.Provide(NewConnection, NewUserRepository),
fx.Decorate(func(log *zap.Logger) *zap.Logger {
return log.Named("db")
}),
)
func main() {
fx.New(
fx.Provide(NewConfig, NewLogger),
DatabaseModule,
HTTPModule,
).Run()
}
将每个模块视为一个小型库,可以提升到另一个应用中——其公共表面是它提供的类型。
关于 fx.Supply/fx.Replace/fx.Decorate、可选依赖、自定义日志、手动生命周期和快速参考,请参见 advanced.md。
最佳实践
- 保持
main()精简——提供者、模块和单个Run()。将实际工作推入模块,以便每个模块可以独立测试。 - 使用生命周期钩子代替
init()或从构造函数启动的 goroutine——Start/Stop 顺序依赖于图拓扑,但init()goroutine 不依赖,这会导致竞态和泄漏。 - OnStart 必须及时返回——长时间工作放在钩子内的 goroutine 中。阻塞的 OnStart 会挂起其余启动过程。
- 在钩子中尊重
ctx.Done()——忽略取消的钩子会被报告为超时失败,但其 goroutine 继续运行,泄漏资源。 - 按模块分组,而非按层——一个模块拥有一个关注点(HTTP、DB、指标)的提供者、生命周期和装饰器。
- 使用
fx.Annotate添加标签,而不是将构造函数包装在fx.Out结构体中——保持构造函数在 fx 外部可复用。 - 对于预构建的值(配置、命令行标志),用
fx.Supply替换fx.Provide。更简洁,表明意图。 - 在 CI 中通过
fx.New(...).Err()启动来验证图——在部署前捕获缺失的提供者和循环。
常见错误
| 错误 | 修复 |
|---|---|
| 长时间运行的工作直接在 OnStart 中 | 在 OnStart 内启动 goroutine;钩子本身必须快速返回,以便依赖的钩子可以运行。 |
fx.Provide 本应是 fx.Supply 的内容 |
预构建的值(配置、密钥)属于 fx.Supply——更清晰,避免无操作的构造函数。 |
| 模块装饰器泄漏到同级 | 在 fx.Module(...) 内部装饰——装饰器仅流向后代。顶层的 fx.Decorate 是全局的。 |
| 假定组顺序 | 组是无序的。如果顺序重要,从一个构造函数提供一个有序切片。 |
| 构造函数带有副作用 | 副作用属于 OnStart——构造函数应轻量且相对纯净,因为它们可能并发且惰性运行。 |
忘记 fx.Invoke |
没有 Invoke(或下游消费者),构造函数永远不会运行。每个应用至少添加一个 Invoke。 |
测试
使用 go.uber.org/fx/fxtest 将 fx 与 *testing.T 集成(失败时调用 t.Fatal,RequireStop 注册为 t.Cleanup)。fx.Populate(&target) 从图中提取值;fx.Replace 将真实依赖替换为模拟。完整模式见 testing.md。
延伸阅读
- advanced.md — Supply/Replace/Decorate、可选依赖、自定义事件日志、手动生命周期、完整快速参考
- recipes.md — 完整的 HTTP 服务(带数据库/指标)、带优雅排空的后台工作进程、同一接口的多个实现、用于 CLI 嵌入的手动生命周期
- testing.md — fxtest 模式、
fx.Replace、fx.Populate、隔离生命周期测试、CI 图验证
交叉引用
- → 参见
samber/cc-skills-golang@golang-uber-dig技能,了解底层容器、dig.In/dig.Out和无生命周期的 DI - → 参见
samber/cc-skills-golang@golang-dependency-injection技能,了解 DI 概念和库比较 - → 参见
samber/cc-skills-golang@golang-samber-do技能,了解基于泛型且无反射的替代方案 - → 参见
samber/cc-skills-golang@golang-google-wire技能,了解编译时 DI(无运行时容器) - → 参见
samber/cc-skills-golang@golang-structs-interfaces技能,了解接口设计模式 - → 参见
samber/cc-skills-golang@golang-context技能,了解 OnStart/OnStop 钩子中的上下文传播 - → 参见
samber/cc-skills-golang@golang-testing技能,了解通用测试模式
如果你遇到 uber-go/fx 中的错误或意外行为,请在 https://github.com/uber-go/fx/issues 提交 issue。





