網站首頁 編程語言 正文
1. 前言
接觸 Golang 有一段時間了,發(fā)現 Golang 同樣需要類似 Java 中 Spring 一樣的依賴注入框架。如果項目規(guī)模比較小,是否有依賴注入框架問題不大,但當項目變大之后,有一個合適的依賴注入框架是十分必要的。通過調研,了解到 Golang 中常用的依賴注入工具主要有 Inject 、Dig 等。但是今天主要介紹的是 Go 團隊開發(fā)的 Wire,一個編譯期實現依賴注入的工具。
2. 依賴注入(DI)是什么
說起依賴注入就要引出另一個名詞控制反轉( IoC )。IoC 是一種設計思想,其核心作用是降低代碼的耦合度。依賴注入是一種實現控制反轉且用于解決依賴性問題的設計模式。
舉個例子,假設我們代碼分層關系是 dal 層連接數據庫,負責數據庫的讀寫操作。那么我們的 dal 層的上一層 service 負責調用 dal 層處理數據,在我們目前的代碼中,它可能是這樣的:
//?dal/user.go func?(u?*UserDal)?Create(ctx?context.Context,?data?*UserCreateParams)?error?{ ????db?:=?mysql.GetDB().Model(&entity.User{}) ????user?:=?entity.User{ ??????Username:?data.Username, ??????Password:?data.Password, ???} ????return?db.Create(&user).Error } //?service/user.go func?(u?*UserService)?Register(ctx?context.Context,?data?*schema.RegisterReq)?(*schema.RegisterRes,?error)?{ ???params?:=?dal.UserCreateParams{ ??????Username:?data.Username, ??????Password:?data.Password, ???} ???err?:=?dal.GetUserDal().Create(ctx,?params) ???if?err?!=?nil?{ ??????return?nil,?err ???} ???registerRes?:=?schema.RegisterRes{ ??????Msg:?"register?success", ???} ???return?®isterRes,?nil }
在這段代碼里,層級依賴關系為 service -> dal -> db,上游層級通過?Getxxx
實例化依賴。但在實際生產中,我們的依賴鏈比較少是垂直依賴關系,更多的是橫向依賴。即我們一個方法中,可能要多次調用Getxxx
的方法,這樣使得我們代碼極不簡潔。
不僅如此,我們的依賴都是寫死的,即依賴者的代碼中寫死了被依賴者的生成關系。當被依賴者的生成方式改變,我們也需要改變依賴者的函數,這極大的增加了修改代碼量以及出錯風險。
接下來我們用依賴注入的方式對代碼進行改造:
//?dal/user.go type?UserDal?struct{ ????DB?*gorm.DB } func?NewUserDal(db?*gorm.DB)?*UserDal{ ????return?&UserDal{ ????????DB:?db ????} } func?(u?*UserDal)?Create(ctx?context.Context,?data?*UserCreateParams)?error?{ ????db?:=?u.DB.Model(&entity.User{}) ????user?:=?entity.User{ ??????Username:?data.Username, ??????Password:?data.Password, ???} ????return?db.Create(&user).Error } //?service/user.go type?UserService?struct{ ????UserDal?*dal.UserDal } func?NewUserService(userDal?dal.UserDal)?*UserService{ ????return?&UserService{ ????????UserDal:?userDal ????} } func?(u?*UserService)?Register(ctx?context.Context,?data?*schema.RegisterReq)?(*schema.RegisterRes,?error)?{ ???params?:=?dal.UserCreateParams{ ??????Username:?data.Username, ??????Password:?data.Password, ???} ???err?:=?u.UserDal.Create(ctx,?params) ???if?err?!=?nil?{ ??????return?nil,?err ???} ???registerRes?:=?schema.RegisterRes{ ??????Msg:?"register?success", ???} ???return?®isterRes,?nil } //?main.go? db?:=?mysql.GetDB() userDal?:=?dal.NewUserDal(db) userService?:=?dal.NewUserService(userDal)
如上編碼情況中,我們通過將 db 實例對象注入到 dal 中,再將 dal 實例對象注入到 service 中,實現了層級間的依賴注入。解耦了部分依賴關系。
在系統(tǒng)簡單、代碼量少的情況下上面的實現方式確實沒什么問題。但是項目龐大到一定程度,結構之間的關系變得非常復雜時,手動創(chuàng)建每個依賴,然后層層組裝起來的方式就會變得異常繁瑣,并且容易出錯。這個時候勇士 wire 出現了!
3. Wire Come
3.1 簡介
Wire 是一個輕巧的 Golang 依賴注入工具。它由 Go Cloud 團隊開發(fā),通過自動生成代碼的方式在編譯期完成依賴注入。它不需要反射機制,后面會看到, Wire 生成的代碼與手寫無異。
3.2 快速使用
wire 的安裝:
go?get?github.com/google/wire/cmd/wire
上面的命令會在?$GOPATH/bin
?中生成一個可執(zhí)行程序?wire
,這就是代碼生成器。可以把$GOPATH/bin
?加入系統(tǒng)環(huán)境變量?$PATH
?中,所以可直接在命令行中執(zhí)行?wire
?命令。
下面我們在一個例子中看看如何使用?wire
。
現在我們有這樣的三個類型:
type?Message?string type?Channel?struct?{ ????Message?Message } type?BroadCast?struct?{ ????Channel?Channel }
三者的 init 方法:
func?NewMessage()?Message?{ ????return?Message("Hello?Wire!") } func?NewChannel(m?Message)?Channel?{ ????return?Channel{Message:?m} } func?NewBroadCast(c?Channel)?BroadCast?{ ????return?BroadCast{Channel:?c} }
假設 Channel 有一個 GetMsg 方法,BroadCast 有一個 Start 方法:
func?(c?Channel)?GetMsg()?Message?{ ????return?c.Message } func?(b?BroadCast)?Start()?{ ????msg?:=?b.Channel.GetMsg() ????fmt.Println(msg) }
如果手動寫代碼的話,我們的寫法應該是:
func?main()?{ ????message?:=?NewMessage() ????channel?:=?NewChannel(message) ????broadCast?:=?NewBroadCast(channel) ????broadCast.Start() }
如果使用?wire
,我們需要做的就變成如下的工作了:
1.提取一個 init 方法 InitializeBroadCast:
func?main()?{ ????b?:=?demo.InitializeBroadCast() ????b.Start() }
2.編寫一個 wire.go 文件,用于 wire 工具來解析依賴,生成代碼:
//+build?wireinject package?demo func?InitializeBroadCast()?BroadCast?{ ????wire.Build(NewBroadCast,?NewChannel,?NewMessage) ????return?BroadCast{} }
注意:需要在文件頭部增加構建約束://+build wireinject
3.使用 wire 工具,生成代碼,在 wire.go 所在目錄下執(zhí)行命令:wire gen wire.go
。會生成如下代碼,即在編譯代碼時真正使用的Init函數:
//?Code?generated?by?Wire.?DO?NOT?EDIT. //go:generate?wire //+build?!wireinject func?InitializeBroadCast()?BroadCast?{ ????message?:=?NewMessage() ????channel?:=?NewChannel(message) ????broadCast?:=?NewBroadCast(channel) ????return?broadCast }
我們告訴?wire
,我們所用到的各種組件的?init
?方法(NewBroadCast
,?NewChannel
,?NewMessage
),那么?wire
?工具會根據這些方法的函數簽名(參數類型/返回值類型/函數名)自動推導依賴關系。
wire.go
?和?wire_gen.go
?文件頭部位置都有一個?+build
,不過一個后面是?wireinject
,另一個是?!wireinject
。+build
?其實是 Go 語言的一個特性。類似 C/C++ 的條件編譯,在執(zhí)行?go build
?時可傳入一些選項,根據這個選項決定某些文件是否編譯。wire
?工具只會處理有wireinject
?的文件,所以我們的?wire.go
?文件要加上這個。生成的?wire_gen.go
?是給我們來使用的,wire
?不需要處理,故有?!wireinject
。
3.3 基礎概念
Wire
?有兩個基礎概念,Provider
(構造器)和?Injector
(注入器)
-
Provider
?實際上就是生成組件的普通方法,這些方法接收所需依賴作為參數,創(chuàng)建組件并將其返回。我們上面例子的?NewBroadCast
?就是?Provider
。 -
Injector
?可以理解為?Providers
?的連接器,它用來按依賴順序調用?Providers
?并最終返回構建目標。我們上面例子的?InitializeBroadCast
?就是?Injector
。
4. Wire使用實踐
下面簡單介紹一下?wire
?在飛書問卷表單服務中的應用。
飛書問卷表單服務的?project
?模塊中將 handler 層、service 層和 dal 層的初始化通過參數注入的方式實現依賴反轉。通過?BuildInjector
?注入器來初始化所有的外部依賴。
4.1 基礎使用
dal 偽代碼如下:
func?NewProjectDal(db?*gorm.DB)?*ProjectDal{ ????return?&ProjectDal{ ????????DB:db ????} } type?ProjectDal?struct?{ ???DB?*gorm.DB } func?(dal?*ProjectDal)?Create(ctx?context.Context,?item?*entity.Project)?error?{ ???result?:=?dal.DB.Create(item) ???return?errors.WithStack(result.Error) } //?QuestionDal、QuestionModelDal...
service 偽代碼如下:
func?NewProjectService(projectDal?*dal.ProjectDal,?questionDal?*dal.QuestionDal,?questionModelDal?*dal.QuestionModelDal)?*ProjectService?{ ???return?&projectService{ ??????ProjectDal:???????projectDal, ??????QuestionDal:??????questionDal, ??????QuestionModelDal:?questionModelDal, ???} } type?ProjectService?struct?{ ???ProjectDal???????*dal.ProjectDal ???QuestionDal??????*dal.QuestionDal ???QuestionModelDal?*dal.QuestionModelDal } func?(s?*ProjectService)?Create(ctx?context.Context,?projectBo?*bo.ProjectCreateBo)?(int64,?error)?{}
handler 偽代碼如下:
func?NewProjectHandler(srv?*service.ProjectService)?*ProjectHandler{ ????return?&ProjectHandler{ ????????ProjectService:?srv ????} } type?ProjectHandler?struct?{ ???ProjectService?*service.ProjectService } func?(s?*ProjectHandler)?CreateProject(ctx?context.Context,?req?*project.CreateProjectRequest)?(resp?* project.CreateProjectResponse,?err?error)?{}
injector.go 偽代碼如下:
func?NewInjector()(handler?*handler.ProjectHandler)?*Injector{ ????return?&Injector{ ????????ProjectHandler:?handler ????} } type?Injector?struct?{ ???ProjectHandler?*handler.ProjectHandler ???//?components,others... }
在 wire.go 中如下定義:
//?+build?wireinject package?app func?BuildInjector()?(*Injector,?error)?{ ???wire.Build( ??????NewInjector, ??????//?handler ??????handler.NewProjectHandler, ??????//?services ??????service.NewProjectService, ??????//?更多service... ??????//dal ??????dal.NewProjectDal, ??????dal.NewQuestionDal, ??????dal.NewQuestionModelDal, ??????//?更多dal... ??????//?db ??????common.InitGormDB, ??????//?other?components... ???) ???return?new(Injector),?nil }
執(zhí)行?wire gen ./internal/app/wire.go
?生成 wire_gen.go
//?Code?generated?by?Wire.?DO?NOT?EDIT. //go:generate?wire //+build?!wireinject func?BuildInjector()?(*Injector,?error)?{ ???db,?err?:=?common.InitGormDB() ???if?err?!=?nil?{ ??????return?nil,?err ???} ??? ???projectDal?:=?dal.NewProjectDal(db) ???questionDal?:=?dal.NewQuestionDal(db) ???questionModelDal?:=?dal.NewQuestionModelDal(db) ???projectService?:=?service.NewProjectService(projectDal,?questionDal,?questionModelDal) ???projectHandler?:=?handler.NewProjectHandler(projectService) ???injector?:=?NewInjector(projectHandler) ???return?injector,?nil }
在 main.go 中加入初始化 injector 的方法?app.BuildInjector
injector,?err?:=?BuildInjector() if?err?!=?nil?{ ???return?nil,?err } //project服務啟動 svr?:=?projectservice.NewServer(injector.ProjectHandler,?logOpt) svr.Run()
注意,如果你運行時,出現了?BuildInjector
?重定義,那么檢查一下你的?//+build wireinject
?與?package app
?這兩行之間是否有空行,這個空行必須要有!見https://github.com/google/wire/issues/117
4.2 高級特性
4.2.1 NewSet
NewSet
?一般應用在初始化對象比較多的情況下,減少?Injector
?里面的信息。當我們項目龐大到一定程度時,可以想象會出現非常多的 Providers。NewSet
?幫我們把這些 Providers 按照業(yè)務關系進行分組,組成?ProviderSet
(構造器集合),后續(xù)只需要使用這個集合即可。
//?project.go var?ProjectSet?=?wire.NewSet(NewProjectHandler,?NewProjectService,?NewProjectDal) //?wire.go func?BuildInjector()?(*Injector,?error)?{ ???wire.Build(InitGormDB,?ProjectSet,?NewInjector) ???return?new(Injector),?nil }
4.2.2 Struct
上述例子的?Provider
?都是函數,除函數外,結構體也可以充當?Provider
?的角色。Wire
?給我們提供了結構構造器(Struct Provider)。結構構造器創(chuàng)建某個類型的結構,然后用參數或調用其它構造器填充它的字段。
//?project_service.go //?函數provider func?NewProjectService(projectDal?*dal.ProjectDal,?questionDal?*dal.QuestionDal,?questionModelDal?*dal.QuestionModelDal)?*ProjectService?{ ???return?&projectService{ ??????ProjectDal:???????projectDal, ??????QuestionDal:??????questionDal, ??????QuestionModelDal:?questionModelDal, ???} } //?等價于 wire.Struct(new(ProjectService),?"*")?//?"*"代表全部字段注入 //?也等價于 wire.Struct(new(ProjectService),?"ProjectDal",?"QuestionDal",?"QuestionModelDal") //?如果個別屬性不想被注入,那么可以修改?struct?定義: type?App?struct?{ ????Foo?*Foo ????Bar?*Bar ????NoInject?int?`wire:"-"` }
4.2.3 Bind
Bind
?函數的作用是為了讓接口類型的依賴參與?Wire
?的構建。Wire
?的構建依靠參數類型,接口類型是不支持的。Bind
?函數通過將接口類型和實現類型綁定,來達到依賴注入的目的。
//?project_dal.go type?IProjectDal?interface?{ ???Create(ctx?context.Context,?item?*entity.Project)?(err?error) ???//?... } type?ProjectDal?struct?{ ???DB?*gorm.DB } var?bind?=?wire.Bind(new(IProjectDal),?new(*ProjectDal))
4.2.4 CleanUp
構造器可以提供一個清理函數(cleanup),如果后續(xù)的構造器返回失敗,前面構造器返回的清理函數都會調用。初始化?Injector
?之后可以獲取到這個清理函數,清理函數典型的應用場景是文件資源和網絡連接資源。清理函數通常作為第二返回值,參數類型為?func()
。當?Provider
?中的任何一個擁有清理函數,Injector
?的函數返回值中也必須包含該函數。并且?Wire
?對?Provider
?的返回值個數及順序有以下限制:
- 第一個返回值是需要生成的對象
- 如果有 2 個返回值,第二個返回值必須是 func() 或 error
- 如果有 3 個返回值,第二個返回值必須是 func(),而第三個返回值必須是 error
//?db.go func?InitGormDB()(*gorm.DB,?func(),?error)?{ ????//?初始化db鏈接 ????//?... ????cleanFunc?:=?func(){ ????????db.Close() ????} ????return?db,?cleanFunc,?nil } //?wire.go func?BuildInjector()?(*Injector,?func(),?error)?{ ???wire.Build( ??????common.InitGormDB, ??????//?... ??????NewInjector ???) ???return?new(Injector),?nil,?nil } //?生成的wire_gen.go func?BuildInjector()?(*Injector,?func(),?error)?{ ???db,?cleanup,?err?:=?common.InitGormDB() ???//?... ???return?injector,?func(){ ???????//?所有provider的清理函數都會在這里 ???????cleanup() ???},?nil } //?main.go injector,?cleanFunc,?err?:=?app.BuildInjector() defer?cleanFunc()
更多用法具體可以參考 wire官方指南:https://github.com/google/wire/blob/main/docs/guide.md
4.3 高階使用
接著我們就用上述的這些?wire
?高級特性對?project
?服務進行代碼改造:
project_dal.go
type?IProjectDal?interface?{ ???Create(ctx?context.Context,?item?*entity.Project)?(err?error) ???//?... } type?ProjectDal?struct?{ ???DB?*gorm.DB } //?wire.Struct方法是wire提供的構造器,"*"代表為所有字段注入值,在這里可以用"DB"代替 //?wire.Bind方法把接口和實現綁定起來 var?ProjectSet?=?wire.NewSet( ???wire.Struct(new(ProjectDal),?"*"), ???wire.Bind(new(IProjectDal),?new(*ProjectDal))) func?(dal?*ProjectDal)?Create(ctx?context.Context,?item?*entity.Project)?error?{}
dal.go
//?DalSet?dal注入 var?DalSet?=?wire.NewSet( ???ProjectSet, ???//?QuestionDalSet、QuestionModelDalSet... )
project_service.go
type?IProjectService?interface?{ ???Create(ctx?context.Context,?projectBo?*bo.CreateProjectBo)?(int64,?error) ???//?... } type?ProjectService?struct?{ ???ProjectDal???????dal.IProjectDal ???QuestionDal??????dal.IQuestionDal ???QuestionModelDal?dal.IQuestionModelDal } func?(s?*ProjectService)?Create(ctx?context.Context,?projectBo?*bo.ProjectCreateBo)?(int64,?error)?{} var?ProjectSet?=?wire.NewSet( ???wire.Struct(new(ProjectService),?"*"), ???wire.Bind(new(IProjectService),?new(*ProjectService)))
service.go
//?ServiceSet?service注入 var?ServiceSet?=?wire.NewSet( ???ProjectSet, ???//?other?service?set... )
handler 偽代碼如下:
var?ProjectHandlerSet?=?wire.NewSet(wire.Struct(new(ProjectHandler),?"*")) type?ProjectHandler?struct?{ ???ProjectService?service.IProjectService } func?(s?*ProjectHandler)?CreateProject(ctx?context.Context,?req?*project.CreateProjectRequest)?(resp?* project.CreateProjectResponse,?err?error)?{}
injector.go 偽代碼如下:
var?InjectorSet?=?wire.NewSet(wire.Struct(new(Injector),?"*")) type?Injector?struct?{ ???ProjectHandler?*handler.ProjectHandler ???//?others... }
wire.go
?//?+build?wireinject package?app func?BuildInjector()?(*Injector,?func(),?error)?{ ???wire.Build( ??????//?db ??????common.InitGormDB, ??????//?dal ??????dal.DalSet, ??????//?services ??????service.ServiceSet, ??????//?handler ??????handler.ProjectHandlerSet, ??????//?injector ??????InjectorSet, ??????//?other?components... ???) ???return?new(Injector),?nil,?nil }
5. 注意事項
5.1 相同類型問題
wire 不允許不同的注入對象擁有相同的類型。google 官方認為這種情況,是設計上的缺陷。這種情況下,可以通過類型別名來將對象的類型進行區(qū)分。
例如服務會同時操作兩個 Redis 實例,RedisA & RedisB
func?NewRedisA()?*goredis.Client?{...} func?NewRedisB()?*goredis.Client?{...}
對于這種情況,wire 無法推導依賴的關系。可以這樣進行實現:
type?RedisCliA?*goredis.Client type?RedisCliB?*goredis.Client func?NewRedisA()?RedicCliA?{...} func?NewRedisB()?RedicCliB?{...}
5.2 單例問題
依賴注入的本質是用單例來綁定接口和實現接口對象間的映射關系。而通常實踐中不可避免的有些對象是有狀態(tài)的,同一類型的對象總是要在不同的用例場景發(fā)生變化,單例就會引起數據的錯誤,不能保存彼此的狀態(tài)。針對這種場景我們通常設計多層的 DI 容器來實現單例隔離,亦或是脫離 DI 容器自行管理對象的生命周期。
6. 結語
Wire 是一個強大的依賴注入工具。與 Inject 、Dig 等不同的是,Wire只生成代碼而不是使用反射在運行時注入,不用擔心會有性能損耗。項目工程化過程中,Wire 可以很好協助我們完成復雜對象的構建組裝。
更多關于 Wire 的介紹請傳送至:https://github.com/google/wire
原文鏈接:https://mp.weixin.qq.com/s/yHB9BzEGIki1fyjYojdpYQ
相關推薦
- 2023-04-19 Android使用gradle讀取并保存數據到BuildConfg流程詳解_Android
- 2022-11-18 C++簡單實現shared_ptr的代碼_C 語言
- 2022-06-14 全面了解C語言?static?關鍵字_C 語言
- 2023-10-09 Cookie和localStorage存儲的區(qū)別
- 2022-12-28 QT?Creator+OpenCV實現圖像灰度化的示例代碼_C 語言
- 2022-06-10 在vscode中快速新建html文件的2種方法總結_C 語言
- 2023-06-20 Python實用技巧之臨時文件的妙用_python
- 2023-12-20 UML類圖中各箭頭表示總結
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支