掘金 后端 ( ) • 2024-05-09 09:52

目录

  • • Fx框架简介

  • • Fx框架能做什么

  • • 实现原理

  • • 使用Fx框架的步骤

  • • 最佳实践及注意事项

  • • 代码示例

  • • 进阶使用经验总结

Fx框架简介

Fx是Uber开发并开源的Go语言模块组合框架,它提供了一种模块化、插拔、可组合的方式来构建Go应用。在没有Fx之前,Go程序需要手动管理依赖,分配和释放资源等,逻辑关系处理变得有些繁琐复杂。引入Fx后,它提供了一种便捷的方式将这些复杂的工作自动化,使得开发工作可以更专注于业务逻辑

Fx框架能做什么

  • • 依赖注入:Fx框架使用了依赖注入理论,Fx的程序通过向Fx容器注册功能函数和组件后,Fx会自动按照依赖顺序进行注入,解决了组件间复杂依赖的问题

  • • 模块化:Fx框架充分发挥了Go包的特性,支持模块化开发,形成功能独立、低耦合的模块,减轻了维护的复杂性

  • • 生命周期管理:Fx提供了生命周期的管理,允许开发者在程序启动阶段初始化资源,以及在程序结束阶段释放和清理资源

实现原理

Fx依赖注入的原理其实是通过反射机制来实现的,具体来说是通过reflect里面的MakeFunc函数来产生相应的函数并调用,从而实现依赖注入的目的。对于生命周期管理,Fx框架会在合适的时机自动地调用注册的函数对组件进行初始化或者释放资源

  1. 1. 依赖注入:Fx框架通过反射地创建对象和解析依赖关系。它维护了一个注册表,存储了所有可以被依赖注入的类和类型信息。当需要一个对象时,Fx会查询注册表,看该类型是否已经在表中注册。如果已注册,Fx会通过反射创建该对象实例,然后找出对象的依赖关系,将已存在注册表中的对应依赖注入到新创建的对象实例中

  2. 2. 生命周期管理:Fx框架通过提供了一系列生命周期事件回调钩子,开发者可以在适当的生命周期事件中插入自定义的操作,如初始化、回收等。在应用程序启动时,Fx会依次调用注册的启动函数,并在应用程序停止时调用注册的停止函数。这些都是通过Fx框架底层的回调钩子来实现,确保在不同阶段可以执行相应的操作

  3. 3. 耦合度降低:Fx框架提供全局对象维护,需在服务创建时进行注入,以达到解耦的目的。在服务需要的时候,Fx会从全局注册表中查找需要的服务,如果找到,就将服务注入到需要的组件中。因此,各个组件不需要知道服务是如何创建的,只需要知道服务的接口,这样就降低了组件之间的耦合度

使用Fx框架的步骤

  1. 1. 创建Fx应用:使用New()创建一个Fx应用,然后通过该应用进行程序的启动和管理

  2. 2. 注册功能函数:利用fx.Provide()函数,将各个组件的构造器注册到Fx的应用容器中。这些构造器函数通常返回我业务中使用的数据库连接、RPC客户端、核心服务等类型的实例,Fx会在运行时调用它们并把返回的对象实例管理起来,供后续步骤使用

  3. 3. 执行业务函数:通过调用fx.Invoke()函数添加业务执行的函数。这些函数是我的业务逻辑的入口,它们可以接收任何我在第二步骤中由fx.Provide()提供的类型作为参数,Fx会将这些类型的实例自动注入到函数中,使得我可以在这些函数中无需手工创建依赖对象,直接使用依赖对象进行业务处理

  4. 4. 启动应用:调用fx.Run()来启动应用。此时,Fx会按照依赖的顺序,调用在第二步注册的构造器函数创建对象实例,然后将这些实例注入到第三步注册的业务执行函数中,并执行这些函数。这一步骤同时也会开始监听系统的中止信号,以确保在收到中止信号后,Fx可以自动管理应用的优雅关闭,包括依赖的逆序关闭和资源的回收

最佳实践及注意事项

最佳实践:

  • • 妥善处理错误和异常:Fx主要通过反射进行函数调用,任何运行时的错误都会导致panic,因此在使用Fx框架时,要确保函数在任何情况下都不会panic,对所有可能的错误进行妥善处理

  • • 使用接口抽象:为了最大化Fx的优点,我们应该尽可能使用接口对服务进行抽象,并只将接口注册到Fx中

  • • 使用Fx生命周期管理:Fx提供了OnStart和OnStop钩子,使用它们可以精细控制服务的启动和关闭,在这些钩子中进行必要的初始化和清理操作

  • • 减少对反射的依赖:虽然反射在某些情况下很有用,但是过度依赖反射可能会导致代码难以理解和维护,同时还可能影响性能。在非必要的情况下,尽量避免使用反射,比如可以使用接口和类型安全的方式替换反射

注意事项:

  • • 避免循环依赖:Fx框架不支持循环依赖,一旦出现循环依赖就会导致panic。因此在设计服务的时候,要注意避免循环依赖

  • • 不要忽视错误:Fx对函数的调用是通过反射进行的,因此任何运行时的错误都可能导致panic,遇到错误要及时处理

代码示例

使用Fx包的生命周期钩子

package main

import (
    "fmt"
    "go.uber.org/fx"
)

// 主函数,创建一个fx应用
func main() {
    fx.New(
        fx.Invoke(registerHooks), // 调用registerHooks函数,这是fx应用的启动项,并且他hook注册到应用的生命周期中
    ).Run() // 运行fx应用
}

// 当fx应用启动时,registerHooks函数将会被调用
func registerHooks(lc fx.Lifecycle) {
    lc.Append(fx.Hook{ // 利用Append方法添加前后钩子
        OnStart: func() error { // OnStart是启动钩子,在应用启动时会触发
            fmt.Println("Starting the application") // 在应用启动时,打印一条消息
            return nil 
        },
        OnStop: func() error { // OnStop是停止钩子,在应用关闭的时候会触发
            fmt.Println("Stopping the application") // 在应用停止时,打印一条消息
            return nil
        },
    })
}

在这个示例中,我们使用fx.Invoke将registerHooks函数加入到Fx的应用中去。registerHooks函数就会在你的Fx应用启动的时候被执行。在registerHooks里面,我们通过lc.Append附加上我们的启动和停止的钩子函数。这样在应用启动时就会打印Starting the application,在应用停止的时候就会打印Stopping the application

使用Fx的依赖注入功能

package main

import (
    "fmt"
    "go.uber.org/fx"
)

// 定义Printer接口,里面包含一个DoPrint方法
type Printer interface {
    DoPrint()
}

// 实现Printer接口的结构体
type MyPrinter struct {}

// MyPrinter结构体的DoPrint方法实现,打印"Printing"
func (p MyPrinter) DoPrint() {
    fmt.Println("Printing")
}

// NewPrinter是 Printer接口的构造函数,返回一个MyPrinter结构体,它实现了Printer接口
func NewPrinter() Printer {
    return MyPrinter{}
}

// 定义一个结构体,它依赖于Printer接口,fx.In用于标记其为依赖项结构体,可以注入到其他函数中
type Params struct {
    fx.In 

    Printer Printer
}

// UsePrinter函数需要一个Params结构体,fx框架会自动将需要的Printer依赖项注入到这个结构体中
func UsePrinter(p Params) {
    p.Printer.DoPrint() // 使用注入的Printer来调用DoPrint方法
}

// main函数,创建一个fx应用
func main() {
    fx.New(
        fx.Provide(NewPrinter), // 提供Printer接口的实现,调用NewPrinter构造函数
        fx.Invoke(UsePrinter),  // 调用UsePrinter函数,fx会自动注入Params结构体所需要的依赖项
    ).Run() // 运行fx应用
}

在这个示例中,首先我们定义了一个Printer接口和其实现类MyPrinter。然后提供了一个NewPrinter的构造函数,用于创建Printer接口的一个实例。由于我们使用了依赖注入,Fx将使用这个函数来自动创建Printer的实现。在UsePrinter函数中,我们需要Printer的实例来执行一些操作,Fx将自动将Printer的实例注入到此函数中。当我们运行此应用时,将自动打印“Printing”

依赖注入和生命周期钩子的结合

定义两个简单的组件:Logger和Calculator

package main

import (
    "fmt"
    "log"
    "go.uber.org/fx"
)

// 组件定义
type Config struct {
    // 配置信息可以放在这里
}

type Service struct {
    config *Config
}

func NewConfig() *Config {
    // 配置信息的初始化逻辑
    return &Config{}
}

func NewService(config *Config) *Service {
    return &Service{config: config}
}

// 应用启动时执行的操作
func (s *Service) OnStart() {
    fmt.Println("Service started with config:", s.config)
}

// 应用停止时执行的操作
func (s *Service) OnStop() {
    fmt.Println("Service stopped")
}

// main 函数
func main() {
    app := fx.New(
        // 提供 Config 的构建方法
        fx.Provide(NewConfig),
        // 提供 Service 的构建方法,并注入 Config 作为依赖
        fx.Provide(NewService),
        // 注册 Service 的 OnStart 和 OnStop 生命周期事件
        fx.Invoke(func(service *Service) {
            service.OnStart()
        }),
        fx.OnStop(func(service *Service) {
            service.OnStop()
        }),
    )

    // 启动应用
    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
    defer app.Stop()

    // 在这里可以进行更多的应用逻辑操作
    fmt.Println("Application is running...")
}

在这个示例中,我们定义了两个组件:Config 和 Service。Config 用于存储配置信息,Service 用于处理业务逻辑。我们通过 fx.Provide 提供了 Config 和 Service 的构造方法,并通过 fx.Invoke 注册了 Service 的 OnStart 和 OnStop 生命周期事件。最后通过 app.Start() 启动应用,app.Stop() 停止应用

进阶使用经验总结

总结使用过程中碰到的一些问题,以及一些进阶用法

FX的组件执行顺序依据依赖关系

  • • fx.Provide()是用来注册构造函数的,而实际的执行是在需要这些构造函数创建的组件时才进行

  • • 当创建新的Uber FX应用时,通过fx.Provide()函数提供的构造函数会被注册到Uber FX的依赖注入容器中,但不会立即执行。这些构造函数将被延迟到fx.Invoke()函数调用或者一个组件需要某个依赖时才执行

  • • 构造函数(通过fx.Provide()注册)的执行顺序是由它们的依赖关系决定的。或者说,只有当一个组件被fx.Invoke()函数引用,或者被其他组件依赖时,该组件的构造函数才会被执行。如果一个组件没有被任何函数或组件引用,那么它的构造函数就不会被执行

这也是为什么fx.Invoke()可以确保在使用某个需要注入的参数的函数被调用时,这个参数对应的组件已经被初始化好:Uber FX会查看该函数参数,找到相应的构造函数,然后再去执行它

package main
import (
"fmt"
"log"
"go.uber.org/fx"
)
type ComponentA struct{}
type ComponentB struct{}
type ComponentC struct{}
// 初始化ComponentA的函数
func NewComponentA() ComponentA {
fmt.Println("Component A is being initialized!")
return ComponentA{}
}
// 初始化ComponentB的函数,需要输入ComponentA作为依赖
func NewComponentB(a ComponentA) ComponentB {
fmt.Println("Component B is being initialized!")
return ComponentB{}
}
// 初始化ComponentC的函数,需要输入ComponentA和ComponentB作为依赖
func NewComponentC(a ComponentA, b ComponentB) ComponentC {
fmt.Println("Component C is being initialized!")
return ComponentC{}
}
// 打印 ComponentA 和 ComponentB 初始化完成的信息
func PrintInitializationDoneForAB(a ComponentA, b ComponentB) {
fmt.Println("Both ComponentA and ComponentB have been initialized!")
}
// 打印 ComponentC 初始化完成的信息
func PrintInitializationDoneForC(c ComponentC) {
fmt.Println("ComponentC has been initialized!")
}
func main() {
app := fx.New(
  fx.Provide(
    // 通过fx.Provide提供组件的初始化函数
    // 这些函数将被注册到Uber FX的依赖注入容器中,但不会立即执行
    NewComponentA,
    NewComponentB,
    NewComponentC,
  ),
  fx.Invoke(
    // 通过fx.Invoke调用函数
    // 在fx.Invoke中的函数将在所有需要的组件初始化完成后才会执行
    PrintInitializationDoneForAB,
    PrintInitializationDoneForC,
  ),
)
// 启动应用
// 启动应用时会按照依赖关系执行fx.Provide中的函数
// 然后执行fx.Invoke中的函数
if err := app.Start(); err != nil {
  log.Fatal(err)
}
}

这个例子中描述了如何通过Uber FX的fx.Provide和fx.Invoke来构建应用和管理应用生命周期。请注意,fx.Provide和fx.Invoke中函数的执行顺序并非是它们声明的顺序,而是由依赖关系决定的。在fx.Invoke中的函数只有在所有需要的组件都初始化完成后才会执行。