Golang?依賴注入經(jīng)典解決方案uber/fx理論解析
開篇

今天繼續(xù)我們的【依賴注入開源解決方案系列】, Dependency Injection 業(yè)界的開源庫非常多,大家可以憑借自己的喜好也業(yè)務(wù)的復(fù)雜度來選型。基于 github star 數(shù)量以及方案的全面性,易用性上。推薦這兩個:
1.【代碼生成】派系推薦大家用 wire, 做的事情非常輕量級,省下大家手寫代碼的負(fù)擔(dān),沒有太多 DI 工具帶來的結(jié)構(gòu)性改造;
2.【反射】派系推薦大家用 uber/fx,功能非常強大,很全面,也比較符合直覺。
二者都需要顯式聲明依賴,這一點對程序的可讀性是好事,兩個庫的 star 也都非常多。建議大家有興趣的話研讀一下。不管是 codegen 還是 reflect(結(jié)合 interface{},泛型)都是 Golang 學(xué)習(xí)體系中必須的能力,否則很難實現(xiàn)通用的一些能力。
今天我們來看看 uber/fx 這個反射派系的經(jīng)典之作,這是 uber 家基于 dig 的又一步進(jìn)化。
uber/fx
Fx is a dependency injection system for Go.
fx 是 uber 2017 年開源的依賴注入解決方案,不僅僅支持常規(guī)的依賴注入,還支持生命周期管理。
從官方的視角看,fx 能為開發(fā)者提供的三大優(yōu)勢:
代碼復(fù)用:方便開發(fā)者構(gòu)建松耦合,可復(fù)用的組件;
消除全局狀態(tài):Fx 會幫我們維護(hù)好單例,無需借用 init() 函數(shù)或者全局變量來做這件事了;
經(jīng)過多年 Uber 內(nèi)部驗證,足夠可信。
我們從 uber-go/fx 看到的是 v1 的版本,fx 是遵循 SemVer 規(guī)范的,保障了兼容性,這一點大家可以放心。
從劣勢的角度分析,其實 uber/fx 最大的劣勢還是大量使用反射,導(dǎo)致項目啟動階段會需要一些性能消耗,但這一般是可以接受的。如果對性能有高要求,建議還是采取 wire 這類 codegen 的依賴注入解法。
目前市面上對 Fx 的介紹文章并不多,筆者在學(xué)習(xí)的時候也啃了很長時間官方文檔,這一點有好有壞。的確,再多的例子,再多的介紹,也不如一份完善的官方文檔更有力。但同時也給初學(xué)者帶來較高的門檻。
今天這篇文章希望從一個開發(fā)者的角度,帶大家理解 Fx 如何使用。
添加 fx 的依賴需要用下面的命令:
go get go.uber.org/fx@v1
后面我們會有專門的一篇文章,拿一個實戰(zhàn)項目來給大家展示,如何使用 Fx,大家同時也可以參考官方 README 中的 Getting Started 來熟悉。
下面一步一步來,我們先來看看 uber/fx 中的核心概念。
provider 聲明依賴關(guān)系
在我們的業(yè)務(wù)服務(wù)的聲明周期中,對于各個 module 的初始化應(yīng)該基于我們的 dependency graph 來合理進(jìn)行。先初始化無外部依賴的對象,隨后基于這些對象,初始化對它們有依賴的對象。
Provider 就是我們常說的構(gòu)造器,能夠提供對象的生成邏輯。在 Fx 啟動時會創(chuàng)建一個容器,我們需要將業(yè)務(wù)的構(gòu)造器傳進(jìn)來,作為 Provider。類似下面這樣:
app = fx.New( fx.Provide(newZapLogger), fx.Provide(newRedisClient), fx.Provide(newMeaningOfLifeCacheRedis), fx.Provide(newMeaningOfLifeHandler), )
這里面的 newXXX 函數(shù),就是我們的構(gòu)造器,類似這樣:
func NewLogger() *log.Logger {
logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */)
logger.Print("Executing NewLogger.")
return logger
}我們只需要通過 fx.Provide 方法傳入進(jìn)容器,就完成了將對象提供出去的使命。隨后 fx 會在需要的時候調(diào)用我們的 Provider,生成單例對象使用。
當(dāng)然,構(gòu)造器不光是這種沒有入?yún)⒌?。還有一些對象是需要顯式的傳入依賴:
func NewHandler(logger *log.Logger) (http.Handler, error) {
logger.Print("Executing NewHandler.")
return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
logger.Print("Got a request.")
}), nil
}注意,這里返回的 http.Handler 也可以成為別人的依賴。這些,我們通通不用關(guān)心!
fx 會自己通過反射,搞明白哪個 Provider 需要什么,能提供什么。構(gòu)建出來整個 dependency graph。
// Provide registers any number of constructor functions, teaching the
// application how to instantiate various types. The supplied constructor
// function(s) may depend on other types available in the application, must
// return one or more objects, and may return an error. For example:
//
// // Constructs type *C, depends on *A and *B.
// func(*A, *B) *C
//
// // Constructs type *C, depends on *A and *B, and indicates failure by
// // returning an error.
// func(*A, *B) (*C, error)
//
// // Constructs types *B and *C, depends on *A, and can fail.
// func(*A) (*B, *C, error)
//
// The order in which constructors are provided doesn't matter, and passing
// multiple Provide options appends to the application's collection of
// constructors. Constructors are called only if one or more of their returned
// types are needed, and their results are cached for reuse (so instances of a
// type are effectively singletons within an application). Taken together,
// these properties make it perfectly reasonable to Provide a large number of
// constructors even if only a fraction of them are used.
//
// See the documentation of the In and Out types for advanced features,
// including optional parameters and named instances.
//
// Constructor functions should perform as little external interaction as
// possible, and should avoid spawning goroutines. Things like server listen
// loops, background timer loops, and background processing goroutines should
// instead be managed using Lifecycle callbacks.
func Provide(constructors ...interface{}) Option {
return provideOption{
Targets: constructors,
Stack: fxreflect.CallerStack(1, 0),
}
}作為開發(fā)者,我們只需要保證,所有我們需要的依賴,都通過 fx.Provide 函數(shù)提供即可。另外需要注意,雖然上面我們是每個 fx.Provide,都只包含一個構(gòu)造器,實際上他是支持多個構(gòu)造器的。
module 模塊化組織依賴
// Module is a named group of zero or more fx.Options.
// A Module creates a scope in which certain operations are taken
// place. For more information, see [Decorate], [Replace], or [Invoke].
func Module(name string, opts ...Option) Option {
mo := moduleOption{
name: name,
options: opts,
}
return mo
}fx 中的 module 也是經(jīng)典的概念。實際上我們在進(jìn)行軟件開發(fā)時,分層分包是不可避免的。而 fx 也是基于模塊化編程。使用 module 能夠幫助我們更方便的管理依賴:
/ ProvideLogger to fx
func ProvideLogger() *zap.SugaredLogger {
logger, _ := zap.NewProduction()
slogger := logger.Sugar()
return slogger
}
// Module provided to fx
var Module = fx.Options(
fx.Provide(ProvideLogger),
)我們的 Module 是一個可導(dǎo)出的變量,包含了一組 fx.Option,這里包含了各個 Provider。
這樣,我們就不必要在容器初始化時傳入那么多 Provider 了,而是每個 Module 干好自己的事即可。
func main() {
fx.New(
fx.Provide(http.NewServeMux),
fx.Invoke(server.New),
fx.Invoke(registerHooks),
loggerfx.Module,
).Run()
}lifecycle 給應(yīng)用生命周期加上鉤子
// Lifecycle allows constructors to register callbacks that are executed on
// application start and stop. See the documentation for App for details on Fx
// applications' initialization, startup, and shutdown logic.
type Lifecycle interface {
Append(Hook)
}
// A Hook is a pair of start and stop callbacks, either of which can be nil.
// If a Hook's OnStart callback isn't executed (because a previous OnStart
// failure short-circuited application startup), its OnStop callback won't be
// executed.
type Hook struct {
OnStart func(context.Context) error
OnStop func(context.Context) error
}lifecycle 是 Fx 定義的一個接口。我們可以對 fx.Lifecycle 進(jìn)行 append 操作,增加鉤子函數(shù),這里就可以支持我們訂閱一些指定行為,如 OnStart 和 OnStop。
如果執(zhí)行某個 OnStart 鉤子時出現(xiàn)錯誤,應(yīng)用會立刻停止后續(xù)的 OnStart,并針對此前已經(jīng)執(zhí)行過 OnStart 的鉤子執(zhí)行對應(yīng)的 OnStop 用于清理資源。
這里 fx 加上了 15 秒的超時限制,通過 context.Context 實現(xiàn),大家記得控制好自己的鉤子函數(shù)執(zhí)行時間。
invoker 應(yīng)用的啟動器
provider 是懶加載的,僅僅 Provide 出來我們的構(gòu)造器,是不會當(dāng)時就觸發(fā)調(diào)用的,而 invoker 則能夠直接觸發(fā)業(yè)務(wù)提供的函數(shù)運行。并且支持傳入一個 fx.Lifecycle 作為入?yún)?,業(yè)務(wù)可以在這里 append 自己想要的 hook。
假設(shè)我們有一個 http server,希望在 fx 應(yīng)用啟動的時候同步開啟。這個時候就需要兩個入?yún)ⅲ?/p>
fx.Lifecycle
我們的主依賴(通常是對服務(wù)接口的實現(xiàn),一個 handler)
我們將這里的邏輯封裝起來,就可以作為一個 invoker 讓 Fx 來調(diào)用了??聪率纠a:
func runHttpServer(lifecycle fx.Lifecycle, molHandler *MeaningOfLifeHandler) {
lifecycle.Append(fx.Hook{OnStart: func(context.Context) error {
r := fasthttprouter.New()
r.Handle(http.MethodGet, "/what-is-the-meaning-of-life", molHandler.Handle)
return fasthttp.ListenAndServe("localhost:8080", r.Handler)
}})
}下面我們將它加入 Fx 容器初始化的流程中:
fx.New(
fx.Provide(newZapLogger),
fx.Provide(newRedisClient),
fx.Provide(newMeaningOfLifeCacheRedis),
fx.Provide(newMeaningOfLifeHandler),
fx.Invoke(runHttpServer),
)這樣在創(chuàng)建容器時,我們的 runHttpServer 就會被調(diào)用,進(jìn)而注冊了服務(wù)啟動的邏輯。這里我們需要一個 MeaningOfLifeHandler,F(xiàn)x 會觀察到這一點,進(jìn)而到 Provider 里面挨個找依賴,每個類型對應(yīng)一個單例對象,通過懶加載的方式獲取到 MeaningOfLifeHandler 的所有依賴,以及子依賴。
其實 Invoker 更多意義上看,像是一個觸發(fā)器。
我們可以有很多 Provider,但什么時候去調(diào)用這些函數(shù),生成依賴呢?Invoker 就是做這件事的。
// New creates and initializes an App, immediately executing any functions // registered via Invoke options. See the documentation of the App struct for // details on the application's initialization, startup, and shutdown logic. func New(opts ...Option) *App
最后,有了一個通過 fx.New 生成的 fx 應(yīng)用,我們就可以通過 Start 方法來啟動了:
func main() {
? ?ctx, cancel := context.WithCancel(context.Background())
? ?kill := make(chan os.Signal, 1)
? ?signal.Notify(kill)
? ?go func() {
? ? ? <-kill
? ? ? cancel()
? ?}()
? ?app := fx.New(
? ? ? fx.Provide(newZapLogger),
? ? ? fx.Provide(newRedisClient),
? ? ? fx.Provide(newMeaningOfLifeCacheRedis),
? ? ? fx.Provide(newMeaningOfLifeHandler),
? ? ? fx.Invoke(runHttpServer),
? ?)
? ?if err := app.Start(ctx);err != nil{
? ? ? fmt.Println(err)
? ?}
}當(dāng)然,有了一個 fx 應(yīng)用后,我們可以直接 fx.New().Run() 來啟動,也可以隨后通過 app.Start(ctx) 方法啟動,配合 ctx 的取消和超時能力。二者皆可。
fx.In 封裝多個入?yún)?/h2>
當(dāng)構(gòu)造函數(shù)參數(shù)過多的時候,我們可以使用 fx.In 來統(tǒng)一注入,而不用在構(gòu)造器里一個個加參數(shù):
type ConstructorParam struct {
? ? fx.In
? ? Logger ?*log.Logger
? ? Handler http.Handler
}
type Object struct {
? ? Logger ?*log.Logger
? ? Handler http.Handler
}
func NewObject(p ConstructorParam) Object {
? ? return Object {
? ? ? ? Logger: ?p.Logger,
? ? ? ? Handler: p.Handler,
? ? }
}fx.Out 封裝多個出參
和 In 類似,有時候我們需要返回多個參數(shù),這時候一個個寫顯然比較笨重。我們可以用 fx.Out 的能力用結(jié)構(gòu)體來封裝:
type Result struct {
? ? fx.Out
? ? Logger ?*log.Logger
? ? Handler http.Handler
}
func NewResult() Result {
? ? // logger := xxx
? ? // handler := xxx
? ? return Result {
? ? ? ? Logger: ?logger,
? ? ? ? Handler: handler,
? ? }
}基于同類型提供多種實現(xiàn)
By default, Fx applications only allow one constructor for each type.
Fx 應(yīng)用默認(rèn)只允許每種類型存在一個構(gòu)造器,這種限制在一些時候是很痛的。
有些時候我們就是會針對一個 interface 提供多種實現(xiàn),如果做不到,我們就只能在外面套一個類型,這和前一篇文章中我們提到的 wire 里的處理方式是一樣的:
type RedisA *redis.Client type RedisB *redis.Client
但這樣還是很笨重,有沒有比較優(yōu)雅的解決方案呢?
當(dāng)然有,要不 uber/fx 怎么能被稱為一個功能全面的 DI 方案呢?
既然是同類型,多個不同的值,我們可以給不同的實現(xiàn)命名來區(qū)分。進(jìn)而這涉及兩個部分:生產(chǎn)端 和 消費端。
在提供依賴的時候,可以聲明它的名稱,進(jìn)而即便出現(xiàn)同類型的其他依賴,fx 也知道如何區(qū)分。
在獲取依賴的時候,也要指明我們需要的依賴的名稱具體是什么,而不只是簡單的明確類型即可。
這里我們需要用到 fx.In 和 fx.Out 的能力。參照 官方文檔 我們來了解一下 fx 的解法:Named Values。
fx 支持開發(fā)者聲明 name 標(biāo)簽,用來給依賴「起名」,類似這樣:name:"rw"。
type GatewayParams struct {
? fx.In
? WriteToConn ?*sql.DB `name:"rw"`
? ReadFromConn *sql.DB `name:"ro" optional:"true"`
}
func NewCommentGateway(p GatewayParams, log *log.Logger) (*CommentGateway, error) {
? if p.ReadFromConn == nil {
? ? log.Print("Warning: Using RW connection for reads")
? ? p.ReadFromConn = p.WriteToConn
? }
? // ...
}
type ConnectionResult struct {
? fx.Out
? ReadWrite *sql.DB `name:"rw"`
? ReadOnly ?*sql.DB `name:"ro"`
}
func ConnectToDatabase(...) (ConnectionResult, error) {
? // ...
? return ConnectionResult{ReadWrite: rw, ReadOnly: ?ro}, nil
}這樣 fx 就知道,我們?nèi)?gòu)建 NewCommentGateway 的時候,傳入的 *sql.DB 需要是 rw 這個名稱的。而此前ConnectToDatabase 已經(jīng)提供了這個名稱,同類型的實例,所以依賴構(gòu)建成功。
使用起來非常簡單,在我們對 In 和 Out 的 wrapper 中聲明各個依賴的 name,也可以搭配 optional 標(biāo)簽使用。fx 支持任意多個 name 的實例。
這里需要注意,同名稱的生產(chǎn)端和消費端的類型必須一致,不能一個是 sql.DB 另一個是 *sql.DB。命名的能力只有在同類型的情況下才有用處。
Annotate 注解器
Annotate lets you annotate a function's parameters and returns without you having to declare separate struct definitions for them.
注解器能幫我們修改函數(shù)的入?yún)⒑统鰠ⅲ瑹o需定義單獨的結(jié)構(gòu)體。fx 的這個能力非常強大,目前暫時沒有看到其他 DI 工具能做到這一點。
func Annotate(t interface{}, anns ...Annotation) interface{} {
result := annotated{Target: t}
for _, ann := range anns {
if err := ann.apply(&result); err != nil {
return annotationError{
target: t,
err: err,
}
}
}
return result
}我們來看看如何用 Annotate 來添加 ParamTag, ResultTag 來實現(xiàn)同一個 interface 多種實現(xiàn)。
// Given,
type Doer interface{ ... }
// And three implementations,
type GoodDoer struct{ ... }
func NewGoodDoer() *GoodDoer
type BadDoer struct{ ... }
func NewBadDoer() *BadDoer
type UglyDoer struct{ ... }
func NewUglyDoer() *UglyDoer
fx.Provide(
? fx.Annotate(NewGoodDoer, fx.As(new(Doer)), fx.ResultTags(`name:"good"`)),
? fx.Annotate(NewBadDoer, fx.As(new(Doer)), fx.ResultTags(`name:"bad"`)),
? fx.Annotate(NewUglyDoer, fx.As(new(Doer)), fx.ResultTags(`name:"ugly"`)),
)這里我們有 Doer 接口,以及對應(yīng)的三種實現(xiàn):GoodDoer, BadDoer, UglyDoer,三種實現(xiàn)的構(gòu)造器返回值甚至都不需要是Doer,完全可以是自己的 struct 類型。
這里還是不得不感慨 fx 強大的裝飾器能力。我們用一個簡單的:
fx.Annotate(NewGoodDoer, fx.As(new(Doer)))
就可以對構(gòu)造器 NewGoodDoer 完成類型轉(zhuǎn)換。
這里還可以寫一個 helper 函數(shù)簡化一下處理:
func AsDoer(f any, name string) any {
? return fx.Anntoate(f, fx.As(new(Doer)), fx.ResultTags("name:" + strconv.Quote(name)))
}
fx.Provide(
?AsDoer(NewGoodDoer, "good"),
?AsDoer(NewBadDoer, "bad"),
?AsDoer(NewUglyDoer, "ugly"),
)與之相對的,提供依賴的時候我們用 ResultTag,消費依賴的時候需要用 ParamTag。
func Do(good, bad, ugly Doer) {
? // ...
}
fx.Invoke(
? fx.Annotate(Do, fx.ParamTags(`name:"good"`, `name:"bad"`, `name:"ugly"`)),
)
這樣就無需通過 fx.In 和 fx.Out 的封裝能力來實現(xiàn)了,非常簡潔。
當(dāng)然,如果我們上面的返回值直接就是 interface,那么久不需要 fx.As 這一步轉(zhuǎn)換了。
go復(fù)制代碼func NewGateway(ro, rw *db.Conn) *Gateway { ... }
fx.Provide(
? fx.Annotate(
? ? NewGateway,
? ? fx.ParamTags(`name:"ro" optional:"true"`, `name:"rw"`),
? ? fx.ResultTags(`name:"foo"`),
? ),
)和下面的實現(xiàn)是等價的:
type params struct {
? fx.In
? RO *db.Conn `name:"ro" optional:"true"`
? RW *db.Conn `name:"rw"`
}
type result struct {
? fx.Out
? GW *Gateway `name:"foo"`
}
fx.Provide(func(p params) result {
? ?return result{GW: NewGateway(p.RO, p.RW)}
})這里需要注意存在兩個限制:
Annotate 不能應(yīng)用于包含 fx.In 和 fx.Out 的函數(shù),它的存在本身就是為了簡化;
不能在一個 Annotate 中多次使用同一個注解,比如下面這個例子會報錯:
fx.Provide(
fx.Annotate(
NewGateWay,
fx.ParamTags(`name:"ro" optional:"true"`),
fx.ParamTags(`name:"rw"), // ERROR: ParamTags was already used above
fx.ResultTags(`name:"foo"`)
)
)小結(jié)
這里是 uber/fx 的理論篇,我們了解了 fx 的核心概念和基礎(chǔ)用法。和 wire 一樣,它們都要求強制編寫構(gòu)造函數(shù),有額外的編碼成本。但好處在于功能全面、設(shè)計比較優(yōu)雅,對業(yè)務(wù)代碼無侵入。
下一篇,我們會從實戰(zhàn)的角度,基于 cloudwego 社區(qū)的 Kitex 框架,看看怎么基于 uber/fx 實現(xiàn)優(yōu)雅的注入,敬請期待。
以上就是 Golang 依賴注入經(jīng)典解決方案 uber/fx 理論篇的詳細(xì)內(nèi)容,更多關(guān)于 Golang 依賴注入經(jīng)典解決方案 uber/fx 理論篇的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言中三個輸入函數(shù)(scanf,scan,scanln)的區(qū)別解析
本文詳細(xì)介紹了Go語言中三個輸入函數(shù)Scanf、Scan和Scanln的區(qū)別,包括用法、功能和輸入終止條件等,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-10-10
Go語言集成開發(fā)環(huán)境之VS Code安裝使用
VS Code是微軟開源的一款編輯器,插件系統(tǒng)十分的豐富,下面介紹如何用VS Code搭建go語言開發(fā)環(huán)境,需要的朋友可以參考下2021-10-10
簡單談?wù)凣olang中的字符串與字節(jié)數(shù)組
這篇文章主要給大家介紹了關(guān)于Golang中字符串與字節(jié)數(shù)組的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用Golang具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
golang實現(xiàn)aes-cbc-256加密解密功能
這篇文章主要介紹了golang實現(xiàn)aes-cbc-256加密解密功能,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04
在?Go?語言中使用?regexp?包處理正則表達(dá)式的操作
正則表達(dá)式是處理字符串時一個非常強大的工具,而?Go?語言的?regexp?包提供了簡單而強大的接口來使用正則表達(dá)式,本文將介紹如何在?Go?中使用?regexp?包來編譯和執(zhí)行正則表達(dá)式,以及如何從文本中匹配和提取信息,感興趣的朋友一起看看吧2023-12-12
Go和Java算法詳析之分?jǐn)?shù)到小數(shù)
這篇文章主要給大家介紹了關(guān)于Go和Java算法詳析之分?jǐn)?shù)到小數(shù)的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-08-08
go引入自建包名報錯:package?XXX?is?not?in?std解決辦法
這篇文章主要給大家介紹了go引入自建包名報錯:package?XXX?is?not?in?std的解決辦法,這是在寫測試引入包名的時候遇到的錯誤提示,文中將解決辦法介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12

