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