Fx 库是 Uber 开源的一个依赖注入(dependency injection)库,Uber 内部大量服务都使用了 Fx 库。Fx 库的实现依赖了 Uber 的另一个依赖注入库:dig 。这篇文章将学习 Fx 库的使用。
Fx 库简介 Fx 是 Go 语言下的一个依赖注入库,通过 Fx,你可以实现如下目标:
减少程序启动时的 boilerplate code
(指那些具有固定模式的代码)
减少程序中的全局状态,不再需要 init()
或者全局变量,而是使用 Fx 管理的单例
增加新的组件后,可以立即在程序中使用它
构建通用的、松耦合的、可共享的模块
Get Started 首先通过一个示例程序来快速了解 Fx 库。
建立最小 Fx 程序
首先新建一个 Go 项目,并安装 Fx 库的最新版本
然后编写一个 Fx 库的最小程序,main.go
如下所示:
1 2 3 4 5 6 7 package mainimport "go.uber.org/fx" func main () { fx.New().Run() }
1 2 3 4 5 [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] RUNNING
在这个最小程序中,我们不带任何参数地调用了 fx.New()
来创建一个 Fx App
。而实际使用中我们通常会给 fx.New()
传递多个参数来启动程序的多个组件。之后通过调用 App.Run()
方法来运行应用程序,该方法会一直阻塞,直到收到停止信号。最后在它退出之前会运行必要的清理操作。
添加一个 HTTP Server 上个最小 Fx 程序没有实现任何功能,接下来我们会给这个程序增加一个 HTTP Server 的功能。
1 2 3 4 func NewHTTPServer (lc fx.Lifecycle) *http.Server { srv := &http.Server{Addr: ":8080" } return srv }
仅仅创建 HTTP server 是不够的,我们还需要告诉 Fx 如何启动该 HTTP server,这也是为什么 NewHTTPServer
需要一个 fx.Lifecycle
。我们可以在 fx.Lifecycle
对象上注册 OnStart hook
、OnStop hook
,通过这两个 hook 我们就可以告诉 Fx 如何启动、停止 HTTP server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func NewHTTPServer (lc fx.Lifecycle) *http.Server { srv := &http.Server{Addr: ":8088" } lc.Append(fx.Hook{ OnStart: func (ctx context.Context) error { ln, err := net.Listen("tcp" , srv.Addr) if err != nil { return err } fmt.Println("Starting HTTP server at" , srv.Addr) go srv.Serve(ln) return nil }, OnStop: func (ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv }
接下来通过 fx.Provide
将该构造函数提供给 Fx App
1 2 3 func main () { fx.New(fx.Provide(NewHTTPServer)).Run() }
仅仅注册 构造器
是不够的。接下来还要通过 fx.Invoke()
注册 触发器
,触发构造 HTTP server
。通常这些 触发器
的参数就是使用由 Provide()
注册的构造器来创建的。可以注册多个 触发器
,这些触发器总是会按注册顺序执行。
1 2 3 4 5 func main () { fx.New(fx.Provide(NewHTTPServer), fx.Invoke(func (*http.Server) {}), ).Run() }
1 2 3 4 5 6 7 8 9 10 11 12 [Fx] PROVIDE *http.Server <= main.NewHTTPServer() [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] INVOKE main.main.func1() [Fx] RUN provide: go.uber.org/fx.New.func1() [Fx] RUN provide: main.NewHTTPServer() [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller : main.NewHTTPServer) Starting HTTP server at :8088 [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 182.506µs [Fx] RUNNING
注册 Handler 接下来给该 HTTP Server 添加 Handler,从而可以真正处理 HTTP 请求。
定义一个 HTTP handler,直接将请求的 body 复制到响应中
1 2 3 4 5 6 7 8 9 10 11 type EchoHandler struct {}func NewEchoHandler () *EchoHandler { return &EchoHandler{} } func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { fmt.Fprintln(os.Stderr, "Failed to handle request:" , err) } }
定义一个构造函数来构造 *http.ServeMux
,*http.ServeMux
用于将 HTTP 请求路由到不同的 handler,这里我们直接将 /echo
的请求路由到 *EchoHandler
,所以这个构造函数接收 *EchoHandler
作为参数
1 2 3 4 5 func NewServeMux (echo *EchoHandler) *http.ServeMux { mux := http.NewServeMux() mux.Handle("/echo" , echo) return mux }
同样,通过 Fx.Provide
将上述构造函数注册到 Fx App
上
1 2 3 4 5 func main () { fx.New(fx.Provide(NewHTTPServer, NewEchoHandler, NewServeMux), fx.Invoke(func (*http.Server) {}), ).Run() }
最后,修改 NewHTTPServer
,让其接受 *http.ServeMux
作为参数
1 2 3 4 func NewHTTPServer (lc fx.Lifecycle, mux *http.ServeMux) *http.Server { srv := &http.Server{Addr: ":8088" , Handler: mux} ...... }
这里我们通过 fx.Provide
提供了更多的组件。Fx 会根据构造函数的参数以及返回值来决定组件之间的依赖关系。
添加 logger 目前程序是在 stdout
和 stderr
输出日志和错误信息,从某种程度上说,stdout
和 stderr
都是全局状态,我们应该将日志信息输出到日志对象。这里使用 Zap
日志库。
首先为 Fx App
提供 Zap logger 的构造函数
1 2 3 4 5 6 func main () { fx.New( fx.Provide(NewHTTPServer, NewEchoHandler, NewServeMux, zap.NewExample), ...... ).Run() }
NewEchoHandler
接受 logger 对象作为参数,并将出错信息输出到 logger
1 2 3 4 5 6 7 8 9 10 11 12 13 type EchoHandler struct { log *zap.Logger } func NewEchoHandler (log *zap.Logger) *EchoHandler { return &EchoHandler{log: log} } func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { h.log.Warn("Failed to handle request:" , zap.Error(err)) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func NewHTTPServer (lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { srv := &http.Server{Addr: ":8088" , Handler: mux} lc.Append(fx.Hook{ OnStart: func (ctx context.Context) error { ln, err := net.Listen("tcp" , srv.Addr) if err != nil { return err } log.Info("Starting HTTP server" , zap.String("addr" , srv.Addr)) go srv.Serve(ln) return nil }, OnStop: func (ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv }
也可以使用 logger 对象来记录 Fx 自己的日志
1 2 3 4 5 6 7 8 9 func main () { fx.New( fx.Provide(NewHTTPServer, NewEchoHandler, NewServeMux, zap.NewExample), fx.Invoke(func (*http.Server) {}), fx.WithLogger(func (log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), ).Run() }
1 2 3 4 {"level" :"info" ,"msg" :"provided" ,"constructor" :"main.NewHTTPServer()" ,"stacktrace" :["main.main (/root/code/private/go/fx/fxdemo/main.go:56)" ,"runtime.main (/usr/local/go/src/runtime/proc.go:250)" ],"moduletrace" :["main.main (/root/code/private/go/fx/fxdemo/main.go:56)" ,"main.main (/root/code/private/go/fx/fxdemo/main.go:55)" ],"type" :"*http.Server" } {"level" :"info" ,"msg" :"provided" ,"constructor" :"main.NewEchoHandler()" ,"stacktrace" :["main.main (/root/code/private/go/fx/fxdemo/main.go:56)" ,"runtime.main (/usr/local/go/src/runtime/proc.go:250)" ],"moduletrace" :["main.main (/root/code/private/go/fx/fxdemo/main.go:56)" ,"main.main (/root/code/private/go/fx/fxdemo/main.go:55)" ],"type" :"*main.EchoHandler" } ......
解耦注册 在上面的例子上,NewServeMux
显式将 EchoHandler
声明为依赖,这带来不必要的耦合性,因为 NewServeMux
并不需要知道某个具体的 handler 实现。接下来将修复这个问题。
1 2 3 4 type Route interface { http.Handler Pattern() string }
EchoHandler
类型实现了 Route 接口
1 2 3 4 func (h *EchoHandler) Pattern() string { return "/echo" }
修改 NewServeMux
接收 Route 类型
1 2 3 4 5 func NewServeMux (route Route) *http.ServeMux { mux := http.NewServeMux() mux.Handle(route.Pattern(), route) return mux }
修改 main 函数,对 NewEchoHandler
进行注解,fx.As()
用于注解函数的返回类型,这样 Fx 就认为 NewEchoHandler
返回的是 type
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { fx.New( fx.Provide( NewHTTPServer, NewServeMux, fx.Annotate(NewEchoHandler, fx.As(new (Route))), zap.NewExample), fx.Invoke(func (*http.Server) {}), fx.WithLogger(func (log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), ).Run() }
这里我们通过接口来让使用者和具体实现解耦,这时用到了 fx.Annotate
、fx.As
来对构造器进行注解。
继续注册 handler 接下来在 HTTP server 中继续增加一个 handler。
首先增加一个 Handler,用于输出 Hello XXX
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type HelloHandler struct { log *zap.Logger } func NewHelloHandler(log *zap.Logger) *HelloHandler { return &HelloHandler{log : log } } func (h *HelloHandler) Pattern() string { return "/hello" } func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { h.log.Error("Failed to read request" , zap.Error(err)) http.Error(w, "Internal server error" , http.StatusInternalServerError) return } if _, err := fmt.Fprintf(w, "Hello, %s\n" , body); err != nil { h.log.Error("Failed to handle request:" , zap.Error(err)) http.Error(w, "Internal server error" , http.StatusInternalServerError) return } }
修改 NewServeMux
,让其可以接受两个 Handler
1 2 3 4 5 6 func NewServeMux (route1, route2 Route) *http.ServeMux { mux := http.NewServeMux() mux.Handle(route1.Pattern(), route1) mux.Handle(route2.Pattern(), route2) return mux }
最后修改 fx.Provide
,增加 NewHelloHandler 这个构造器
1 2 3 4 5 6 7 8 fx.Provide( NewHTTPServer, NewServeMux, fx.Annotate(NewEchoHandler, fx.As(new (Route))), fx.Annotate(NewHelloHandler, fx.As(new (Route))), zap.NewExample),
但是直接运行,会报错,其中最关键的原因就是 Route
实例被重复注册。Fx App
内不允许同一个类型的两个实例同时存在,为了解决这个问题,我们仍然需要使用注解以区分两个不同的 Route
实例。这里我们用 fx.ResultTags
来为构造器的结果增加 tag 注解
1 2 3 4 5 6 7 8 9 10 11 fx.Provide( ...... fx.Annotate( NewEchoHandler, fx.As(new (Route)), fx.ResultTags(`name:"echo"` )), fx.Annotate( NewHelloHandler, fx.As(new (Route)), fx.ResultTags(`name:"hello"` )), zap.NewExample),
同样的,由于 NewServeMux
接受两个 Route 实例作为参数,为了将它们区分,使用 fx.ParamTags
对构造器的参数增加 tag 注解
1 2 3 4 5 6 fx.Provide( ...... fx.Annotate( NewServeMux, fx.ParamTags(`name:"echo"` , `name:"hello"` )), .....),
所以可以看到 fx.ResultTags
和 fx.ParamTags
是配套使用的。fx.ResultTags
对构造器结果增加 Tag 注解,fx.ParamTags
则是对构造器参数增加 Tag 注解,从而在 Fx App
中支持同一个类型的多个实例。这些被注解的实例也被称为 Named Values
(需要使用 name:".."
tag 注解)。
注册多个 Handler 接下来我们继续扩展该程序,让 NewServeMux
可以接受多个 handler。
首先修改 NewServeMux
让其可以接收 Route 的 slice
1 2 3 4 5 6 7 func NewServeMux (routes []Route) *http.ServeMux { mux := http.NewServeMux() for _, route := range routes { mux.Handle(route.Pattern(), route) } return mux }
接下来注解 NewServeMux
,表明其接受一个 Value Groups
来作为参数。Fx 使用 group:".."
这个注解 tag 来表明 slice 参数是一个 Value Groups
,这会使得 Fx 执行所有的、会往该 group 中产生 Value 的 provider(执行顺序不确定),并将得到的结果保存到该 slice 参数中。除了使用 slice 作为参数,Value Groups
也支持 可变参数
1 2 3 4 5 6 7 fx.Provide( ...... fx.Annotate( NewServeMux, fx.ParamTags(`group:"routes"` )), ...... )
然后定义一个 Helper 函数 AsRoute
来让 Route 的 Provider 可以往这个 Value Groups
中产生 Value,这是通过 fx.ResultTags(group:"routes"
) 实现的:
1 2 3 4 5 6 7 func AsRoute (f any) any { return fx.Annotate( f, fx.As(new (Route)), fx.ResultTags(`group:"routes"` ), ) }
最后借助 AsRoute
,将已有的 Route Provider 注册到 Fx App
中
1 2 3 4 5 6 7 8 fx.Provide( NewHTTPServer, fx.Annotate( NewServeMux, fx.ParamTags(`group:"routes"` )), AsRoute(NewEchoHandler), AsRoute(NewHelloHandler), zap.NewExample),
这一步我们主要借助 Value Groups
来让 NewServeMux
可以接收一组 handler 作为参数。需要注意,如果 Value Groups
的构造函数返回类型 T,那么 Value Groups
的消费者其参数类型应该是 []T
。
Fx 其他特性 上一小节通过一个实例介绍了 Fx 主要特性,接下来继续介绍一些没有覆盖到的 Fx 重要特性。
参数对象
在上面的示例中,构造函数都是通过参数来声明它自己的依赖。如果构造函数有很多依赖,并都通过函数参数的方式来指明这些依赖,代码的可读性就会越来越差,例如:
1 2 3 func NewHandler (users *UserGateway, comments *CommentGateway, posts *PostGateway, votes *VoteGateway, authz *AuthZGateway) *Handler { }
参数对象
(Parameter Objects
)就是用来解决这个问题的:定义一个结构体并将函数的所有依赖定义为该结构体的字段,之后函数以 该结构体
作为参数,这样就解决了函数参数过多带来的代码可读性问题。
Fx 中,通过在结构体中嵌入 Fx.In
来定义 参数对象
。如下是一个简单示例:
定义一个结构体,例如 ClientParams
(通常 Fx 的 参数对象
只用于作为构造器的参数,不要把它用于通用的业务逻辑)
1 2 type ClientParams struct {}
1 2 3 type ClientParams struct { fx.In }
1 2 func NewClient (p ClientParams) (*Client, error ) {}
在 参数对象
结构体中添加真正的依赖类型字段,其需要是结构体的导出字段:
1 2 3 4 5 6 type ClientParams struct { fx.In Config ClientConfig HTTPClient *http.Client }
1 2 3 4 5 6 7 func NewClient (p ClientParams) (*Client, error ) { return &Client{ url: p.Config.URL, http: p.HTTPClient, }, nil }
当我们需要给构造函数增加新的依赖时,只需要在 参数对象
中接续添加新的字段。如果需要实现向后兼容,可以为新增的字段添加 optional:"true"
1 2 3 4 5 6 7 type Params struct { fx.In Config ClientConfig HTTPClient *http.Client Logger *zap.Logger `optional:"true"` }
之后在构造函数中继续处理新增的依赖。注意如果没有提供该字段,那么其是对应类型的 零值
1 2 3 4 5 6 7 func New (p Params) (*Client, error ) { log := p.Logger if log == nil { log = zap.NewNop() } }
参数对象
通过 name:"..." tag
来支持 named value
,通过 group:"..."
来支持 value groups
1 2 3 4 5 6 7 8 9 10 11 12 type GatewayParams struct { fx.In WriteToConn *sql.DB `name:"rw"` ReadFromConn *sql.DB `name:"ro"` } type ServerParams struct { fx.In Handlers []Handler `group:"server"` }
结果对象
与参数对象对应的是 结果对象
(Result Objects),用于指定函数的返回类型。通过在结构体中新增 fx.Out
字段,这样就可以定义 结果对象
。
如下是一个示例:
定义一个结构体,例如 ClientResult
(Fx 的 结果对象
只用于作为构造器的返回类型,不要把它用于通用的业务逻辑)
1 2 type ClientResult struct {}
1 2 3 type ClientResult struct { fx.Out }
1 2 func NewClient () (ClientResult, error ) {}
在 结果对象
类型中新增真正的生成字段,需要是导出字段
1 2 3 4 5 type ClientResult struct { fx.Out Client *Client }
1 2 3 4 5 6 func NewClient () (ClientResult, error ) { client := &Client{ } return ClientResult{Client: client}, nil }
1 2 3 4 5 6 7 8 9 10 11 12 type ConnectionResult struct { fx.Out ReadWrite *sql.DB `name:"rw"` ReadOnly *sql.DB `name:"ro"` } type HandlerResult struct { fx.Out Handler Handler `group:"server"` }
应用程序生命周期 Fx App
的生命周期可以分为两个阶段:initialization
和 execution
。每个阶段包含多个步骤,在 initialization
期间,包含以下步骤:
注册 fx.Provide
提供的所有构造函数
注册 fx.Decorate
提供的所有装饰函数
运行 fx.Invoke
指定的所有函数
在 execution
期间,Fx 执行如下步骤:
运行所有的 OnStart hooks
,这些 hooks 可以是由 providers
、decorators
、invoked 函数
中注册的
等待程序停止运行的信号
运行所有的 OnStop hooks
模块 Fx 模块是一个可共享的 Go 库或包,可以给 Fx App
提供自包含的功能。如下提供一个简单示例:
首先通过 fx.Module()
定义一个 Module
类型的顶层变量
1 var Module = fx.Module("server" ,
1 2 3 4 5 6 var Module = fx.Module("server" , fx.Provide( New, parseConfig, ), )
如果模块总是运行某个函数,可以使用 fx.Invoke
1 2 3 4 5 6 7 var Module = fx.Module("server" , fx.Provide( New, parseConfig, ), fx.Invoke(startServer), )
在使用依赖之前,也支持使用 fx.Decorate
来装饰这些依赖
1 2 3 4 5 6 7 8 var Module = fx.Module("server" , fx.Provide( New, parseConfig, ), fx.Invoke(startServer), fx.Decorate(wrapLogger), )
如果你只想让你构造函数的结果只限于本模块使用(还有你模块所包含的子模块),可以在 Provide 的时候使用 fx.Private
。如下 parseConfig
只对 server
模块有效,其他模块不能使用这个构造函数
1 2 3 4 5 6 7 8 9 10 11 12 var Module = fx.Module("server" , fx.Provide( New, ), fx.Provide( fx.Private, parseConfig, ), fx.Invoke(startServer), fx.Decorate(wrapLogger), )
以下列举了一些在 Uber 中使用 Fx 模块的约定。
可以作为独立包发布的 Fx 模块需要以 fx 作为命名后缀,包内的 Fx 模块可以不用这个后缀
参数对象
以 Params
后缀命名、结果对象
以 Result
后缀命名
模块正常工作所依赖的函数(这些函数通常需要通过 fx.Provide
或 fx.Invoke
注册)需要被导出,这是为了保证即使没有 Fx,我们的模块也可以被使用
模块暴露的函数需要以 参数对象
作为参数,而不是直接接受多个参数,这样可以保持可扩展性
类似地,模块暴露的函数需要以 结果对象
作为返回值,而不是直接返回多个值
只提供你真正能提供的类型,不要提供本模块的 依赖
,也不要捆绑其他模块
Fx 模块应该很少包含业务逻辑。如果一个 Fx 模块包含了真正的业务逻辑,它不应该以 fx
后缀命名。这样做的好处是让用户可以自由决定是否使用 Fx
谨慎使用 Invoke
,因为 fx.Invoke
注册的函数总是无条件运行,它不像 fx.Provide
注册的构造函数总是在真正需要的时候才运行
Reference