在上一篇文章中,我们介绍了 fx 框架的基本用法,并展示了如何使用 fx 构建一个简单的服务。相信大家现在已经掌握了使用 fx 创建和管理依赖注入的基本方法以及启动应用程序的方法。为了让你的项目更加专业和高效,我们接下来将深入探讨 fx 框架的高级功能和使用技巧,如如何利用 fx.Lifecycle 管理服务生命周期,在应用启动和停止时执行特定逻辑,以及如何使用 fx.Invoke 注册启动时需要调用的函数。通过了解这些高级功能,你将能够充分发挥 fx 的潜力,构建出更加复杂和健壮的 Go 应用程序。

让我们一起探索 fx 的高级用法,提升你的编程技巧和项目质量。

fx.Invoke

上一篇文章,我们讲到 fx.Invoke 方法可以注册 fx.Lifecycle 中的 hook ,用来进行生命周期的管理。接下来我们介绍另外一个用法:用于注册需要依赖注入的函数,这些函数会在应用程序启动时调用,并且会自动接收所需的依赖项。

也就是说通过 fx.Invoke 调用一些函数在程序启动时实例化某些依赖对象。具体来说,fx.Invoke 注册的函数会在应用程序启动时被调用,这些函数的参数会自动由 fx 提供的依赖注入机制解析并注入。初始化调用有点类似 Springboot 中的 org.springframework.boot.CommandLineRunner#run ,不同的是 Springboot 会在这个方法之前实例化各个 Component 对象,而 fx 默认的是调用时候才会初始化。所以如果想在程序启动的时候初始化一些资源或者对象,就可以通过调用 fx.Invoke 方法实现。

下面是一个简单的例子:

package main  
  
import (  
    "go.uber.org/fx"  
    "go.uber.org/zap")  
  
func main() {  
    app := fx.New( //创建fx.App实例  
       fx.Provide(NewTester, func() *Age {  
          return &Age{Num: 18} //提供Age实例  
       }, func() *zap.Logger {  
          production, _ := zap.NewProduction() //提供zap.Logger实例  
          return production  
       }), //提供NewTester函数  
       fx.Invoke(func(*Tester) {  
          //调用Tester函数,默认会调用对应的 provide 方法中提供的函数,如果不需要实际调用对象,可以不写形参的名称  
       }),   
)  
    app.Run() //运行fx.App实例  
}  
  
type Age struct {  
    Num int //年龄,整型  
}  
  
type Tester struct {  
    Log *zap.Logger //日志  
    Age *Age        //年龄  
}  
  
func NewTester(age *Age, log *zap.Logger) *Tester {  
    return &Tester{  
       Age: age,  
       Log: log,  
    }  
  
}

fx.Invoke 注册了一个匿名函数,该函数接收一个 Tester 类型的参数。fx 容器会确保在应用启动时,Tester 及其所有依赖(Age 和 zap.Logger)都被实例化。即使匿名函数中不使用 Tester 对象,fx 仍会调用 NewTester 以确保 Tester 被正确创建和初始化。

fx.Supply

fx.Supply 方法用于直接向 fx 框架 provide 一个对象,不用通过方法注入。主要的使用场景如下:

使用场景

  • 静态配置:你已经有了一个配置对象,可以直接将其提供给 Fx。
  • 现有实例:你有一些已经创建好的实例,可以直接注入,而不需要通过 fx.Provide。
  • 测试对象:在单元测试中,你可以使用 fx.Supply 提供一些测试对象。

下面是个简单的例子:

package main  
  
import (  
    "go.uber.org/fx"  
    "go.uber.org/zap")  
  
func main() {  
    app := fx.New( //创建fx.App实例  
       fx.Provide(func() *zap.Logger {  
          production, _ := zap.NewProduction() //提供zap.Logger实例  
          return production  
       }), //提供NewTester函数  
       fx.Supply(&Age{Num: 18}), //提供Age实例  
    )  
    app.Run() //运行fx.App实例  
}  
  
type Age struct {  
    Num int //年龄,整型  
}  
  
type Tester struct {  
    Log *zap.Logger //日志  
    Age *Age        //年龄  
}  
  
func NewTester(age *Age, log *zap.Logger) *Tester {  
    return &Tester{  
       Age: age,  
       Log: log,  
    }  
  
}

fx.popular

fx.PopulateFx 框架中的一个功能,用于将依赖注入到外部的变量中。它可以让你在应用启动时,将 fx 容器中的依赖直接注入到你指定的变量中,而不需要在构造函数或初始化逻辑中显式地传递这些依赖。

意思就是使用这个方法,传入一些对象的指针,然后就可以在程序启动的时候初始化创建实例了。

*需要注意的是 Populate(targets …interface{}) 中传入的targets必须得是目标类型TypeX的指针类型 TypeX,哪怕 TypeX 本身就是指针类型

下面是 fx.popular 两种使用场景:

  • 外部变量注入:需要将 fx 容器中的依赖注入到外部的全局变量或其他作用域中。
  • 测试:在单元测试中,可以方便地将依赖注入到测试用例中,便于进行依赖的替换和注入。

下面来展示一下代码:

package main  
  
import (  
    "go.uber.org/fx"  
    "go.uber.org/zap")  
  
var (  
    logger *zap.Logger  
    age    *Age  
)  
  
func main() {  
    app := fx.New(  
       fx.Provide(  
          NewLogger,  
          NewAge,  
       ),  
       fx.Populate(&logger, &age),  
       fx.Invoke(func() {  
          logger.Info("Application started", zap.Int("age", age.Num))  
       }),  
    )  
    app.Run()  
}  
  
func NewLogger() (*zap.Logger, error) {  
    return zap.NewProduction()  
}  
  
func NewAge() *Age {  
    return &Age{Num: 30}  
}  
  
type Age struct {  
    Num int  
}

fx.popular 方法的优势体现在下面三个方面:

  • 简化依赖注入:fx.Populate 提供了一种简洁的方式,将依赖注入到外部变量中,避免了在构造函数或初始化逻辑中显式地传递这些依赖。
  • 提高代码可读性:通过使用全局变量或特定作用域的变量,可以使代码更加直观和易读。
  • 测试友好:在测试环境中,可以方便地替换和注入依赖,便于进行单元测试和集成测试。

fx.Annotated

fx.AnnotatedFx 框架中的一个功能,用于向依赖注入容器提供带有特定标签的构造函数。这在处理依赖注入时非常有用,特别是当你有多个相同类型的实例,但需要将它们区分开来时。

当我们依赖的对象类型相同时候,可以用 fx.Annotated 方法进行对象的区分,比如我们同时要记录多种日志、连接多个数据库等等。不仅仅要在注入依赖对象的时候进行区分,也需要在使用的时候进行区分。

下面是个例子:

package main  
  
import (  
    "go.uber.org/fx"  
    "go.uber.org/zap")  
  
type Age struct {  
    Num int  
}  
  
type Tester struct {  
    Log *zap.Logger  
    Age *Age 
}  
  
var Ages = fx.Provide(  
    fx.Annotated{  
       Name:   "old",  
       Target: NewAgeOld,  
    },  
    fx.Annotated{  
       Name:   "young",  
       Target: NewAgeYoung,  
    })  
  
func main() {  
    app := fx.New(  
       fx.Provide(  
          NewLogger,  
          fx.Annotate(  
             NewTester,  
             fx.ParamTags(`name:"old"`),  
          ),  
       ),  
       Ages,  
       fx.Invoke(  
          func(t *Tester) {  
             t.Log.Info("sussess", zap.Int("age", t.Age.Num))  
          },  
       ),  
    )  
    app.Run()  
}  
  
func NewLogger() (*zap.Logger, error) {  
    return zap.NewProduction()  
}  
  
func NewTester(age *Age, log *zap.Logger) *Tester {  
    return &Tester{Log: log, Age: age}  
}  
  
func NewAgeYoung() *Age {  
    return &Age{Num: 18}  
}  
  
func NewAgeOld() *Age {  
    return &Age{Num: 60}  
}

fx.Annotated 还需要搭配 fx.Annotate 才能将参数传给创建的方法,制定同一类型对象的具体某个实例。fx.Annotated 的构造方法中还有一个参数 GroupName 不能被同时使用。下面是 Group 的例子。

package main  
  
import (  
    "fmt"  
    "go.uber.org/fx")  
  
type Age interface {  
    Print() //无返回值  
}  
  
type AgeOld struct {  
}  
  
type AgeYoung struct {  
}  
  
func (age *AgeOld) Print() {  
    fmt.Println("old")  
}  
  
func (age *AgeYoung) Print() {  
    fmt.Println("young")  
}  
  
type Man struct {  
    Ages []Age `group:"men"`  
}  
  
func NewApp(ages []Age) *Man {  
    return &Man{Ages: ages}  
}  
  
var Ages = fx.Provide(  
    fx.Annotated{  
       Group:  "men",  
       Target: NewAgeOld,  
    },  
    fx.Annotated{  
       Group:  "men",  
       Target: NewAgeYoung,  
    })  
  
func main() {  
    app := fx.New(  
       Ages,  
       fx.Provide(  
          fx.Annotate(  
             NewApp,  
             fx.ParamTags(`group:"men"`),  
          ),  
       ),  
       fx.Invoke(func(man *Man) {  
          ages := man.Ages  
          for _, age := range ages {  
             age.Print()  
          }  
       }),  
    )  
    app.Run()  
}  
func NewAgeYoung() Age {  
    return &AgeYoung{}  
}  
  
func NewAgeOld() Age {  
    return &AgeOld{}  
}

fx.In 和 fx.Out

fx.Infx.Out 是 Uber 的 fx 依赖注入框架中的两个重要结构,用于管理复杂的依赖注入场景。一般在构建大型项目的时候,会经常用到 fx.Infx.Out ,对于提升代码整洁度和可维护性有很重要的作用,同时也能够避免依赖注入异常的发生。

fx.In

fx.In 用于聚合多个输入参数到一个结构体中。当我们使用 fx.In 结构体时,就无需在 fx.New() 方法中显示定义构造方法了。此时,只要当前结构体的依赖对象均在 fx 框架中定义,就可以直接创建当前的结构体对象。其主要特点是:

  • 允许将多个依赖组合成一个单一的参数
  • 支持可选依赖
  • 可以使用标签来指定特定的依赖

演示的代码如下:

package main  
  
import (  
    "fmt"  
    "go.uber.org/fx")  
  
type Name struct {  
    Str string  
}  
  
type Age struct {  
    Num int  
}  
  
type Tester struct {  
    fx.In  
    Age  *Age  
    Name *Name  
}  
  
func main() {  
    NewAge := func() *Age {  
       return &Age{Num: 30}  
    }  
    NewName := func() *Name {  
       return &Name{Str: "FunTester"}  
    }  
    app := fx.New(  
       fx.Provide(  
          NewAge,  
          NewName,  
       ),  
       fx.Invoke(func(t Tester) {  
          fmt.Println(t.Name.Str)  
       }),  
    )  
    app.Run()  
}

fx.Out

fx.Out 用于从一个函数返回多个值,这些值可以被注入到其他地方。当我们使用 fx.Out 定义一个结构体,那么当我们初始化这个结构体对象的时候,它所依赖的属性对象也会自动 providefx 框架当中。其主要特点是:

  • 允许一个函数提供多个依赖
  • 支持使用标签来命名或分组输出

下面是演示代码:

package main  
  
import (  
    "fmt"  
    "go.uber.org/fx")  
  
type Name struct {  
    Str string  
}  
  
type Age struct {  
    Num int  
}  
  
// Values 表示两个返回值:年龄和姓名  
type Values struct {  
    fx.Out  
    Age  *Age  
    Name *Name  
}  
  
func NewValues() Values {  
    return Values{  
       Age:  &Age{Num: 30},  
       Name: &Name{Str: "FunTester"},  
    }  
}  
  
func main() {  
    app := fx.New(  
       // 提供构造函数  
       fx.Provide(  
          NewValues, // 使用 NewValues 而不是单独的 NewAge 和 NewName       ),  
       fx.Invoke(func(age *Age) {  
          fmt.Println(age.Num)  
       }),  
    )  
    app.Run()  
}

结语

到这里,常用的功能都覆盖了,当前 fx 的内容不仅仅是这些,但对于学习、开发一个 Go 项目已经足够了。相信只要不断前进,早晚会用到更高级的语法。下面我列一下我学习过程中未在文章中列举的 API

fx.module

fx.Modulefx 框架中的一个功能,用于组织和封装相关的依赖和功能。它允许开发者将一组相关的提供者(providers)和调用者(invokers)打包成一个独立的单元。这些模块可以被命名、重用和组合,从而简化大型应用的结构。fx.Module 支持嵌套和条件加载,提高了代码的模块化程度、可维护性和可测试性。它特别适用于构建复杂、可扩展的Go应用程序,使得依赖管理和功能组织变得更加清晰和高效。

fx.withlogger

fx.WithLogger 允许自定义 fx 框架的日志记录器。通过提供一个函数,该函数接收标准日志记录器并返回 fxevent.Logger,你可以替换默认的日志记录器,实现特定需求的日志记录。例如,可以集成 zap.Logger,使 fx 使用 zap 进行一致的日志记录,从而提高调试和监控的效果。

fx.As

fx.Asfx 包中的一个选项,用于将具体类型转换为其接口类型进行依赖注入。通过 fx.As,你可以在 fx.Provide 中指定将某个构造函数的返回值作为接口类型提供,使得依赖注入更加灵活和可扩展。这有助于实现松耦合和增强代码的可测试性。