fx 框架上手 - 進階篇

FunTester發表於2024-08-01

在上一篇文章中,我們介紹了 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 中指定將某個建構函式的返回值作為介面型別提供,使得依賴注入更加靈活和可擴充套件。這有助於實現松耦合和增強程式碼的可測試性。

  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go、Python
  • 單元&白盒&工具合集
  • 測試方案&BUG&爬蟲&UI
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章