Golang?依賴注入經(jīng)典解決方案uber/fx理論解析
開篇
今天繼續(xù)我們的【依賴注入開源解決方案系列】, Dependency Injection 業(yè)界的開源庫非常多,大家可以憑借自己的喜好也業(yè)務的復雜度來選型。基于 github star 數(shù)量以及方案的全面性,易用性上。推薦這兩個:
1.【代碼生成】派系推薦大家用 wire, 做的事情非常輕量級,省下大家手寫代碼的負擔,沒有太多 DI 工具帶來的結(jié)構(gòu)性改造;
2.【反射】派系推薦大家用 uber/fx,功能非常強大,很全面,也比較符合直覺。
二者都需要顯式聲明依賴,這一點對程序的可讀性是好事,兩個庫的 star 也都非常多。建議大家有興趣的話研讀一下。不管是 codegen 還是 reflect(結(jié)合 interface{},泛型)都是 Golang 學習體系中必須的能力,否則很難實現(xiàn)通用的一些能力。
今天我們來看看 uber/fx 這個反射派系的經(jīng)典之作,這是 uber 家基于 dig 的又一步進化。
uber/fx
Fx is a dependency injection system for Go.
fx 是 uber 2017 年開源的依賴注入解決方案,不僅僅支持常規(guī)的依賴注入,還支持生命周期管理。
從官方的視角看,fx 能為開發(fā)者提供的三大優(yōu)勢:
代碼復用:方便開發(fā)者構(gòu)建松耦合,可復用的組件;
消除全局狀態(tài):Fx 會幫我們維護好單例,無需借用 init() 函數(shù)或者全局變量來做這件事了;
經(jīng)過多年 Uber 內(nèi)部驗證,足夠可信。
我們從 uber-go/fx 看到的是 v1 的版本,fx 是遵循 SemVer 規(guī)范的,保障了兼容性,這一點大家可以放心。
從劣勢的角度分析,其實 uber/fx 最大的劣勢還是大量使用反射,導致項目啟動階段會需要一些性能消耗,但這一般是可以接受的。如果對性能有高要求,建議還是采取 wire 這類 codegen 的依賴注入解法。
目前市面上對 Fx 的介紹文章并不多,筆者在學習的時候也啃了很長時間官方文檔,這一點有好有壞。的確,再多的例子,再多的介紹,也不如一份完善的官方文檔更有力。但同時也給初學者帶來較高的門檻。
今天這篇文章希望從一個開發(fā)者的角度,帶大家理解 Fx 如何使用。
添加 fx 的依賴需要用下面的命令:
go get go.uber.org/fx@v1
后面我們會有專門的一篇文章,拿一個實戰(zhàn)項目來給大家展示,如何使用 Fx,大家同時也可以參考官方 README 中的 Getting Started 來熟悉。
下面一步一步來,我們先來看看 uber/fx 中的核心概念。
provider 聲明依賴關(guān)系
在我們的業(yè)務服務的聲明周期中,對于各個 module 的初始化應該基于我們的 dependency graph 來合理進行。先初始化無外部依賴的對象,隨后基于這些對象,初始化對它們有依賴的對象。
Provider 就是我們常說的構(gòu)造器,能夠提供對象的生成邏輯。在 Fx 啟動時會創(chuàng)建一個容器,我們需要將業(yè)務的構(gòu)造器傳進來,作為 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 方法傳入進容器,就完成了將對象提供出去的使命。隨后 fx 會在需要的時候調(diào)用我們的 Provider,生成單例對象使用。
當然,構(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)典的概念。實際上我們在進行軟件開發(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 是一個可導出的變量,包含了一組 fx.Option,這里包含了各個 Provider。
這樣,我們就不必要在容器初始化時傳入那么多 Provider 了,而是每個 Module 干好自己的事即可。
func main() { fx.New( fx.Provide(http.NewServeMux), fx.Invoke(server.New), fx.Invoke(registerHooks), loggerfx.Module, ).Run() }
lifecycle 給應用生命周期加上鉤子
// 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 進行 append 操作,增加鉤子函數(shù),這里就可以支持我們訂閱一些指定行為,如 OnStart 和 OnStop。
如果執(zhí)行某個 OnStart 鉤子時出現(xiàn)錯誤,應用會立刻停止后續(xù)的 OnStart,并針對此前已經(jīng)執(zhí)行過 OnStart 的鉤子執(zhí)行對應的 OnStop 用于清理資源。
這里 fx 加上了 15 秒的超時限制,通過 context.Context 實現(xiàn),大家記得控制好自己的鉤子函數(shù)執(zhí)行時間。
invoker 應用的啟動器
provider 是懶加載的,僅僅 Provide 出來我們的構(gòu)造器,是不會當時就觸發(fā)調(diào)用的,而 invoker 則能夠直接觸發(fā)業(yè)務提供的函數(shù)運行。并且支持傳入一個 fx.Lifecycle 作為入?yún)ⅲ瑯I(yè)務可以在這里 append 自己想要的 hook。
假設我們有一個 http server,希望在 fx 應用啟動的時候同步開啟。這個時候就需要兩個入?yún)ⅲ?/p>
fx.Lifecycle
我們的主依賴(通常是對服務接口的實現(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)用,進而注冊了服務啟動的邏輯。這里我們需要一個 MeaningOfLifeHandler,F(xiàn)x 會觀察到這一點,進而到 Provider 里面挨個找依賴,每個類型對應一個單例對象,通過懶加載的方式獲取到 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 應用,我們就可以通過 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) ? ?} }
當然,有了一個 fx 應用后,我們可以直接 fx.New().Run() 來啟動,也可以隨后通過 app.Start(ctx) 方法啟動,配合 ctx 的取消和超時能力。二者皆可。
fx.In 封裝多個入?yún)?/h2>
當構(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 應用默認只允許每種類型存在一個構(gòu)造器,這種限制在一些時候是很痛的。
有些時候我們就是會針對一個 interface 提供多種實現(xiàn),如果做不到,我們就只能在外面套一個類型,這和前一篇文章中我們提到的 wire 里的處理方式是一樣的:
type RedisA *redis.Client type RedisB *redis.Client
但這樣還是很笨重,有沒有比較優(yōu)雅的解決方案呢?
當然有,要不 uber/fx 怎么能被稱為一個功能全面的 DI 方案呢?
既然是同類型,多個不同的值,我們可以給不同的實現(xiàn)命名來區(qū)分。進而這涉及兩個部分:生產(chǎn)端 和 消費端。
在提供依賴的時候,可以聲明它的名稱,進而即便出現(xiàn)同類型的其他依賴,fx 也知道如何區(qū)分。
在獲取依賴的時候,也要指明我們需要的依賴的名稱具體是什么,而不只是簡單的明確類型即可。
這里我們需要用到 fx.In 和 fx.Out 的能力。參照 官方文檔 我們來了解一下 fx 的解法:Named Values。
fx 支持開發(fā)者聲明 name 標簽,用來給依賴「起名」,類似這樣: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 標簽使用。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)⒑统鰠?,無需定義單獨的結(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 接口,以及對應的三種實現(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)了,非常簡潔。 當然,如果我們上面的返回值直接就是 interface,那么久不需要 fx.As 這一步轉(zhuǎn)換了。 go復制代碼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 不能應用于包含 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 的核心概念和基礎用法。和 wire 一樣,它們都要求強制編寫構(gòu)造函數(shù),有額外的編碼成本。但好處在于功能全面、設計比較優(yōu)雅,對業(yè)務代碼無侵入。
下一篇,我們會從實戰(zhàn)的角度,基于 cloudwego 社區(qū)的 Kitex 框架,看看怎么基于 uber/fx 實現(xiàn)優(yōu)雅的注入,敬請期待。
以上就是 Golang 依賴注入經(jīng)典解決方案 uber/fx 理論篇的詳細內(nèi)容,更多關(guān)于 Golang 依賴注入經(jīng)典解決方案 uber/fx 理論篇的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言中三個輸入函數(shù)(scanf,scan,scanln)的區(qū)別解析
本文詳細介紹了Go語言中三個輸入函數(shù)Scanf、Scan和Scanln的區(qū)別,包括用法、功能和輸入終止條件等,本文給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-10-10Go語言集成開發(fā)環(huán)境之VS Code安裝使用
VS Code是微軟開源的一款編輯器,插件系統(tǒng)十分的豐富,下面介紹如何用VS Code搭建go語言開發(fā)環(huán)境,需要的朋友可以參考下2021-10-10簡單談談Golang中的字符串與字節(jié)數(shù)組
這篇文章主要給大家介紹了關(guān)于Golang中字符串與字節(jié)數(shù)組的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者使用Golang具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-03-03golang實現(xiàn)aes-cbc-256加密解密功能
這篇文章主要介紹了golang實現(xiàn)aes-cbc-256加密解密功能,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04go引入自建包名報錯:package?XXX?is?not?in?std解決辦法
這篇文章主要給大家介紹了go引入自建包名報錯:package?XXX?is?not?in?std的解決辦法,這是在寫測試引入包名的時候遇到的錯誤提示,文中將解決辦法介紹的非常詳細,需要的朋友可以參考下2023-12-12