在上一篇文章中,我们介绍了 fx
框架的基本用法,并展示了如何使用 fx
构建一个简单的服务。相信大家现在已经掌握了使用 fx 创建和管理依赖注入的基本方法以及启动应用程序的方法。为了让你的项目更加专业和高效,我们接下来将深入探讨 fx
框架的高级功能和使用技巧,如如何利用 fx.Lifecycle
管理服务生命周期,在应用启动和停止时执行特定逻辑,以及如何使用 fx.Invoke
注册启动时需要调用的函数。通过了解这些高级功能,你将能够充分发挥 fx
的潜力,构建出更加复杂和健壮的 Go
应用程序。
让我们一起探索 fx
的高级用法,提升你的编程技巧和项目质量。
上一篇文章,我们讲到 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
框架 provide
一个对象,不用通过方法注入。主要的使用场景如下:
使用场景
下面是个简单的例子:
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.Populate
是 Fx
框架中的一个功能,用于将依赖注入到外部的变量中。它可以让你在应用启动时,将 fx
容器中的依赖直接注入到你指定的变量中,而不需要在构造函数或初始化逻辑中显式地传递这些依赖。
意思就是使用这个方法,传入一些对象的指针,然后就可以在程序启动的时候初始化创建实例了。
需要注意的是 Populate(targets …interface{}) 中传入的 targets 必须得是目标类型 TypeX 的指针类型 *TypeX,哪怕 TypeX 本身就是指针类型
下面是 fx.popular
两种使用场景:
下面来展示一下代码:
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.Annotated
是 Fx
框架中的一个功能,用于向依赖注入容器提供带有特定标签的构造函数。这在处理依赖注入时非常有用,特别是当你有多个相同类型的实例,但需要将它们区分开来时。
当我们依赖的对象类型相同时候,可以用 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
的构造方法中还有一个参数 Group
和 Name
不能被同时使用。下面是 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
是 Uber 的 fx 依赖注入框架中的两个重要结构,用于管理复杂的依赖注入场景。一般在构建大型项目的时候,会经常用到 fx.In
和 fx.Out
,对于提升代码整洁度和可维护性有很重要的作用,同时也能够避免依赖注入异常的发生。
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
定义一个结构体,那么当我们初始化这个结构体对象的时候,它所依赖的属性对象也会自动 provide
到 fx
框架当中。其主要特点是:
下面是演示代码:
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
框架中的一个功能,用于组织和封装相关的依赖和功能。它允许开发者将一组相关的提供者(providers
)和调用者(invokers
)打包成一个独立的单元。这些模块可以被命名、重用和组合,从而简化大型应用的结构。fx.Module
支持嵌套和条件加载,提高了代码的模块化程度、可维护性和可测试性。它特别适用于构建复杂、可扩展的Go
应用程序,使得依赖管理和功能组织变得更加清晰和高效。
fx.WithLogger
允许自定义 fx
框架的日志记录器。通过提供一个函数,该函数接收标准日志记录器并返回 fxevent.Logger
,你可以替换默认的日志记录器,实现特定需求的日志记录。例如,可以集成 zap.Logger
,使 fx
使用 zap
进行一致的日志记录,从而提高调试和监控的效果。
fx.As
是 fx
包中的一个选项,用于将具体类型转换为其接口类型进行依赖注入。通过 fx.As
,你可以在 fx.Provide
中指定将某个构造函数的返回值作为接口类型提供,使得依赖注入更加灵活和可扩展。这有助于实现松耦合和增强代码的可测试性。