后端开发工具【免费下载链接】fxA dependency injection based application framework for Go.项目地址https://gitcode.com/gh_mirrors/fx1/fx点击查看免费下载导读本文围绕 Uber 开源的依赖注入框架 Fx本仓库即其源码模块导入路径为go.uber.org/fx的核心特性Value Groups值组展开。值组允许任意数量的构造函数向同一个同名集合投递值、任意数量的消费者按需读取这些值双方互不知道对方的全貌是 Fx 中实现可插拔组件注册如 HTTP 路由、插件、监听器等的标准手段。读完本文你将掌握值组标签的完整语法、通过结果对象/参数对象与fx.Annotate注解两种投递与消费方式、严格依赖与软依赖soft的区别与选择以及值组展平flatten等进阶用法。什么是 Value Groups值组是同一类型的值的集合。在一个 Fx 应用中任意数量的构造函数可以把值投递feed进某个值组任意数量的消费者可以读取consume该值组而无需知道全部生产者是谁。这一机制让模块之间实现了解耦新增一个生产者只需要在新模块里把值标上组标签并fx.Provide所有消费该组的组件会自动收到这个新值无需改动任何消费者代码。官方文档用一个 HTTP 路由注册场景来说明其形态docs/src/value-groups/index.mdNewA、NewB……NewZ等任意多个构造函数各自产出一个Route值投递到[]Route值组NewServeMux和NewSiteMap作为消费者分别读取整组路由完成注册。生产者无需感知彼此消费者也无需枚举全部生产者——新增路由只需新增一个构造函数。重要提醒Fx 以随机顺序产生值组中的值。绝不要对值组内值的顺序做任何假设详见下文无序性一节。值组的核心标签语法值组通过结构体标签struct tag识别标签键为group场景声明方式标签示例投递结果侧在fx.Out结果对象的导出字段上用T类型标注Watcher Watcher \group:watchers消费输入侧在fx.In参数对象的导出字段上用[]T切片类型标注Watchers []Watcher \group:watchers软依赖消费输入侧追加,soft选项Handlers []Handler \group:server,soft展平投递结果侧追加,flatten选项Handler []Handler \group:server,flatten在 annotated.go 的标签校验逻辑中可以看到Fx 只接受group、name、optional三种合法标签键标签必须符合key:value且多个键值对之间以空格分隔的 Go 结构体标签规范validKeys : map[string]struct{}{group: {}, optional: {}, name: {}}投递侧要求值是类型T消费侧要求是[]T——如果生产者产出[]T消费者就需要消费[][]T除非使用,flatten展平。向值组投递值Feeding要向类型为T的值组投递值只需把T类型的结果标记上group:$name$name为值组名。官方提供了两种方式docs/src/value-groups/feed.md结果对象和注解函数。方式一通过结果对象Result Objects当函数返回一个嵌入fx.Out的结果对象时可以在其中标注要投递的字段。前置条件一个产生结果对象的构造函数并已提供给 Fx 应用。// docs/ex/value-groups/feed/result.go type Result struct { fx.Out Watcher Watcher group:watchers } func New( /* ... */ ) (Result, error) { watcher : watcher{ /* ... */ } return Result{ Watcher: watcher, }, nil }fx.Provide(New)操作步骤在结果对象中新增一个导出字段类型为要投递的值类型并打上值组名的标签如上例Watcher Watcher \group:watchers在构造函数中把该字段赋值为要投递的值。完整示例见 docs/ex/value-groups/feed/result.go。关于结果对象的更多细节可参考 docs/src/result-objects.md。方式二通过注解函数fx.Annotate fx.ResultTags当函数返回值类型与组一致、但不想改动函数签名时可以用fx.Annotate包装函数并追加fx.ResultTags注解。前置条件一个产出组所需类型值的函数并已提供。// docs/ex/value-groups/feed/annotate.go func NewWatcher( /* ... */ ) (Watcher, error) { // ... return FileWatcher{ /* ... */ }, nil }fx.Provide(NewWatcher)操作步骤用fx.Annotate包裹传给fx.Provide的函数用fx.ResultTags声明其结果投递到值组。fx.Provide( fx.Annotate( NewWatcher, fx.ResultTags(group:watchers), ), )从源码看ResultTags会在反射层面把函数结果改造成一个等价于内嵌fx.Out、字段带标签的结构体见 annotated.go 中resultTagsAnnotation的apply/build实现因此它与手写结果对象在语义上是等价的。类型不必完全一致用 fx.As 转型投递如果被注解的函数产出的类型不是组的元素类型但只要可以被转换为该类型例如把具体结构体转型为接口仍然可以投递进值组// docs/ex/value-groups/feed/annotate.go func NewFileWatcher( /* ... */ ) (*FileWatcher, error) { return FileWatcher{ /* ... */ }, nil } fx.Provide( fx.Annotate( NewFileWatcher, fx.As(new(Watcher)), fx.ResultTags(group:watchers), ), )这里*FileWatcher通过fx.As(new(Watcher))转型为Watcher接口再投递进watchers组。关于结构体到接口转型的更多说明见 docs/src/annotate.md#casting-structs-to-interfaces。从值组消费值Consuming消费类型T的值组需要在参数侧声明[]T切片并标记group:$name。同样有两种方式docs/src/value-groups/consume.md参数对象和注解函数。方式一通过参数对象Parameter Objects前置条件一个消费参数对象的函数并已提供。// docs/ex/value-groups/consume/param.go type Params struct { fx.In Watchers []Watcher group:watchers } func New(p Params) (Result, error) { for _, w : range p.Watchers { // ... } return Result{ Emitter: Emitter{ws: p.Watchers}, }, nil }fx.Provide(New)操作步骤在参数对象中新增一个导出字段类型为[]TT为组内值的类型打上值组名标签在函数中直接消费该切片。参数对象的底层约定可参考 docs/src/parameter-objects.md。其运行机制与投递侧的fx.Out对称Fx 会先执行所有向该组投递值的构造函数顺序不确定再把所有结果收集进一个切片注入该字段见 doc.go。警告不要依赖切片内值的顺序——顺序是随机的见下文无序性。方式二通过注解函数fx.Annotate fx.ParamTags前置条件一个接受[]T切片参数的函数并已提供。func NewEmitter(watchers []Watcher) (*Emitter, error) { for _, w : range watchers { // ... } return Emitter{ws: watchers}, nil } fx.Provide(NewEmitter)操作步骤用fx.Annotate包裹函数用fx.ParamTags声明其切片参数来自值组在函数体内消费该切片。fx.Provide( fx.Annotate( NewEmitter, fx.ParamTags(group:watchers), ), )ParamTags的源码实现annotated.go会把函数参数改造成一个等价于内嵌fx.In、字段带标签的结构体再在调用时把字段解包回原参数列表因此对原函数签名零侵入。注意ParamTags不可用于参数本身就是fx.In结构体的函数annotated.go。进阶变参函数消费值组如果函数接受变参variadic而非切片同样可以消费值组——把变参当作切片来注解即可// docs/ex/value-groups/consume/annotate.go func EmitterFrom(watchers ...Watcher) (*Emitter, error) { return Emitter{ws: watchers}, nil } fx.Provide( fx.Annotate( EmitterFrom, fx.ParamTags(group:watchers), ), )依赖严格性严格值组与软值组由值组形成的依赖关系可以是两种docs/src/value-groups/index.md严格strict生产值组的构造函数总是被消费执行软soft只有当对应构造函数因为别的原因被 Fx 调用时其产出才会被纳入值组。默认是严格依赖。严格值组Strict严格值组依赖的构造无论其生产者是否被应用的其他部分使用都会被值组消费。假设构造函数NewFoo产出两个值A和BA投递进值组[]A该值组被函数Run消费应用通过fx.Invoke(Run)调用Run在严格值组下Fx 会无条件执行NewFoo来填充[]A组——即使应用根本没有直接或间接消费它的另一个产出B。这正是值组形成强依赖的体现声明了消费就保证生产者会被实例化。软值组Soft软值组的依赖只有在生产者构造函数恰好被 Fx 调用时才会被组消费——即应用直接或间接消费了该构造函数的其他产出。沿用上面的例子只是把值组改成软依赖此时 Fx仅在A或B被应用中另一组件直接或间接消费时才会执行NewFoo来填充[]A。如果应用中只有Run依赖[]A而B无人消费那么NewFoo根本不会被调用上面这张图里B被C消费、C被Start消费于是NewFoo会被执行A也因此进入[]A组——但这是顺带的而非值组强制要求的。声明方式在输入侧组标签上追加,soft选项该选项只对输入参数有效type Params struct { fx.In Handlers []Handler group:server,soft Logger *zap.Logger } func NewServer(p Params) *Server { // ... }doc.go 用一段对比进一步说明了软值组的语义假设有两个构造函数都向server组投递值func NewHandlerAndLogger() (Handler, *zap.Logger) { // ... } func NewHandler() Handler { // ... } fx.Provide( fx.Annotate(NewHandlerAndLogger, fx.ResultTags(group:server)), fx.Annotate(NewHandler, fx.ResultTags(group:server)), )如果应用其他地方消费了*zap.Logger那么NewHandlerAndLogger会被调用、其Handler会进入server组而NewHandler的产出只被软值组消费因此它不会被调用。软值组可以被理解为对已运行构造函数的尽力收集best-effort它不会主动触发任何生产者只会把那些碰巧被实例化的生产者产出收集进组。它非常适合可选组件场景——某个可选组件想向组里贡献值但只有当它确实被应用其余部分用到时才贡献。值组展平Flattening默认约定是生产者产出T消费者消费[]T如果生产者产出[]T消费者就必须消费[][]T。但在生产者想一次性产出多个值、消费者希望以单个切片[]T接收的场景下Fx 提供了展平值组在结果侧的组标签上追加,flatten选项表示切片中的每个元素单独注入值组type HandlerResult struct { fx.Out Handler []Handler group:server,flatten // 消费方在 ServerParams 中仍以 []Handler 接收。 }type ServerParams struct { fx.In Handlers []Handler group:server }这样生产者一次产出整批Handler消费者依然以扁平的[]Handler接收全部值见 doc.go。同样的,flatten选项也出现在fx.Annotated的Group字段说明中annotated.go即注解路径同样支持展平。无序性为什么不能依赖值组顺序官方文档与源码都反复强调值组中的值是无序的Fx 对值的产生顺序不做任何保证docs/src/value-groups/index.md、doc.go。这意味着多个生产者都会被调用但调用顺序不确定消费端拿到的[]T切片中元素顺序是随机的可能与构造函数执行顺序不一致任何依赖先注册先生效、按添加顺序排列之类的业务逻辑都是错误的。如果你需要确定性顺序应当在消费端自行排序例如按元素自身的某个字段排序而不是依赖注入顺序。用测试验证值组行为仓库为投递与消费示例都配备了可运行的测试可作为理解与复用的范本。投递测试docs/ex/value-groups/feed/feed_test.go同时验证了结果对象注解函数转型投递三种方式每种方式投递一个Watcher然后用fx.Populate把[]Watcher \group:watchers 填充出来并断言长度为 1func TestWatcherModules(t *testing.T) { tests : []struct { name string module fx.Option }{ {Results, ResultModule}, {Annotated, AnnotateModule}, {FileWatcher, FileWatcherModule}, } for _, tt : range tests { t.Run(tt.name, func(t *testing.T) { var out struct { fx.In Watchers []Watcher group:watchers } app : fxtest.New(t, tt.module, fx.Populate(out), ) app.RequireStart().RequireStop() assert.Len(t, out.Watchers, 1) }) } }消费测试docs/ex/value-groups/consume/consume_test.go则验证了三种消费方式向watchers组提供 3 个Watcher后无论用参数对象、fx.ParamTags注解还是变参函数最终Emitter持有的ws长度都为 3func TestConsume(t *testing.T) { tests : []struct { name string module fx.Option }{ {Params, ParamsModule}, {Annotated, AnnotateModule}, {EmitterFrom, EmitterFromModule}, } addWatcher : fx.Annotate( func() struct{} { return struct{}{} }, fx.As(new(Watcher)), fx.ResultTags(group:watchers), ) for _, tt : range tests { t.Run(tt.name, func(t *testing.T) { var e *Emitter app : fxtest.New(t, tt.module, fx.Provide(addWatcher, addWatcher, addWatcher), fx.Populate(e), ) app.RequireStart().RequireStop() assert.Len(t, e.ws, 3) }) } }这两个测试共同证明了投递与消费的多种写法结果对象/注解函数、参数对象/注解函数/变参在语义上完全等价、可自由混用——你可以用结果对象投递、用fx.ParamTags消费反之亦然。实战建议小结优先选一种风格并保持一致小项目推荐结果对象 参数对象类型自文档化不想改动函数签名时用fx.Annotatefx.ResultTags/fx.ParamTags。默认严格按需软依赖绝大多数场景如路由注册用默认的严格值组即可只有可选组件用了才贡献时才使用group:...,soft。永远不要依赖顺序消费端如需有序自行排序。批量投递用,flatten一次产出多个值、又希望消费端收到扁平切片时使用group:...,flatten。组合模块化每个生产者可放在独立模块fx.Options中通过 docs/src/modules.md 介绍的模块机制自由组合新增组件零侵入。赞分享后端开发工具【免费下载链接】fxA dependency injection based application framework for Go.项目地址https://gitcode.com/gh_mirrors/fx1/fx点击查看免费下载相关推荐Crossbeam Channel高性能多生产者多消费者消息通道Crossbeam Channel高性能多生产者多消费者消息通道 Crossbeam Channel 是 Rust 生态中高性能的并发消息传递库其 API并发编程内存管理Uber FX终极指南Go依赖注入框架完整教程Uber FX终极指南Go依赖注入框架完整教程 Uber FX是一款基于依赖注入的Go应用程序框架专为构建可维护的微服务架构而设计。作为Uber内部大量Go后端开发工具Uber FX框架终极指南Go依赖注入的完整教程Uber FX框架终极指南Go依赖注入的完整教程 Uber FX是一个基于依赖注入的Go应用框架它能帮助开发者构建模块化、可测试且易于维护的Go应用程序。本后端开发工具上一篇技术深度解析TranslucentTB系统集成工具部署失败与权限冲突解决方案下一篇Home Assistant 30 分钟跑通一张 SD 卡装好智能家居中枢新手最易踩的 3 个坑创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
