深入淺出go依賴注入工具Wire的使用
前言
在日常項(xiàng)目開發(fā)中,我們經(jīng)常會(huì)使用到依賴注入的設(shè)計(jì)模式,目的是為了降低代碼組件之間的耦合度,提高代碼的可維護(hù)性、可擴(kuò)展性和可測(cè)試性。
但隨著項(xiàng)目規(guī)模的增長(zhǎng),組件之間的依賴關(guān)系變得復(fù)雜,手動(dòng)管理它們之間的依賴關(guān)系可能會(huì)很繁瑣。為了簡(jiǎn)化這個(gè)過程,我們可以利用依賴注入代碼生成工具,它可以自動(dòng)為我們生成所需的代碼,從而減輕了手動(dòng)處理依賴注入的繁重工作。
Go
語(yǔ)言有許多依賴注入的工具,而本文將深入探討一個(gè)備受歡迎的 Go
語(yǔ)言依賴注入工具—— Wire
。
準(zhǔn)備好了嗎?準(zhǔn)備一杯你最喜歡的咖啡或茶,隨著本文一探究竟吧。
Wire
Wire
是一個(gè)專為依賴注入(Dependency Injection
)設(shè)計(jì)的代碼生成工具,它可以自動(dòng)生成用于初始化各種依賴關(guān)系的代碼,從而幫助我們更輕松地管理和注入依賴關(guān)系。
Wire 安裝
我們可以執(zhí)行以下命令來安裝 Wire
工具:
go install github.com/google/wire/cmd/wire@latest
安裝之前請(qǐng)確保已將 $GOPATH/bin
添加到環(huán)境變量 $PATH
里。
Wire 的基本使用
前置代碼準(zhǔn)備
雖然我們?cè)谇懊嬉呀?jīng)通過 go install
命令安裝了 Wire
命令行工具,但在具體項(xiàng)目中,我們?nèi)匀恍枰ㄟ^以下命令安裝項(xiàng)目所需的 Wire
依賴,以便結(jié)合 Wire
工具生成代碼:
go get github.com/google/wire@latest
接下來,讓我們模擬一個(gè)簡(jiǎn)單的 web
博客項(xiàng)目,編寫查詢文章接口的相關(guān)代碼,并使用 Wire
工具生成代碼。
首先,我們先定義相關(guān)類型與方法,并提供對(duì)應(yīng)的 初始化函數(shù):
定義 PostHandler
結(jié)構(gòu)體,創(chuàng)建注冊(cè)路由的方法 RegisterRoutes
和查詢文章路由處理的方法 GetPostById
以及初始化的函數(shù) NewPostHandler
,并且它依賴于 IPostService
接口:
package handler import ( "chenmingyong0423/blog/tutorial-code/wire/internal/post/service" "github.com/gin-gonic/gin" "net/http" ) type PostHandler struct { serv service.IPostService } func (h *PostHandler) RegisterRoutes(engine *gin.Engine) { engine.GET("/post/:id", h.GetPostById) } func (h *PostHandler) GetPostById(ctx *gin.Context) { content := h.serv.GetPostById(ctx, ctx.Param("id")) ctx.String(http.StatusOK, content) } func NewPostHandler(serv service.IPostService) *PostHandler { return &PostHandler{serv: serv} }
定義 IPostService
接口,并提供了一個(gè)具體實(shí)現(xiàn) PostService
,接著創(chuàng)建 GetPostById
方法,用于處理查詢文章的邏輯,然后提供初始化函數(shù) NewPostService
,該函數(shù)返回 IPostService
接口類型:
package service import ( "context" "fmt" ) type IPostService interface { GetPostById(ctx context.Context, id string) string } var _ IPostService = (*PostService)(nil) type PostService struct { } func (s *PostService) GetPostById(ctx context.Context, id string) string { return fmt.Sprint("歡迎關(guān)注本掘金號(hào),作者:陳明勇") } func NewPostService() IPostService { return &PostService{} }
定義一個(gè)初始化 gin.Engine
函數(shù) NewGinEngineAndRegisterRoute
,該函數(shù)依賴于 *handler.PostHandler
類型,函數(shù)內(nèi)部調(diào)用相關(guān) handler
結(jié)構(gòu)體的方法創(chuàng)建路由:
package ioc import ( "chenmingyong0423/blog/tutorial-code/wire/internal/post/handler" "github.com/gin-gonic/gin" ) func NewGinEngineAndRegisterRoute(postHandler *handler.PostHandler) *gin.Engine { engine := gin.Default() postHandler.RegisterRoutes(engine) return engine }
使用 Wire 工具生成代碼
前置代碼已經(jīng)準(zhǔn)備好了,接下來我們編寫核心代碼,以便 Wire
工具能生成相應(yīng)的依賴注入代碼。
首先我們需要?jiǎng)?chuàng)建一個(gè) wire
的配置文件,通常命名為 wire.go
。在這個(gè)文件里,我們需要定義一個(gè)或者多個(gè)注入器函數(shù)(Injector
函數(shù),接下來的內(nèi)容會(huì)對(duì)其進(jìn)行解釋),以便指引 Wire
工具生成代碼。
//go:build wireinject package wire import ( "chenmingyong0423/blog/tutorial-code/wire/internal/post/handler" "chenmingyong0423/blog/tutorial-code/wire/internal/post/service" "chenmingyong0423/blog/tutorial-code/wire/ioc" "github.com/gin-gonic/gin" "github.com/google/wire" ) func InitializeApp() *gin.Engine { wire.Build( handler.NewPostHandler, service.NewPostService, ioc.NewGinEngineAndRegisterRoute, ) return &gin.Engine{} }
在上述代碼中,我們定義了一個(gè)用于初始化 gin.Engine
的注入器函數(shù),在該函數(shù)內(nèi)部,我們使用了 wire.Build
方法來聲明依賴關(guān)系,其中包括 PostHandler
、PostService
和 InitGinEngine
作為依賴的構(gòu)造函數(shù)。
wire.Build
的作用是 連接或綁定我們之前定義的所有初始化函數(shù)。當(dāng)我們運(yùn)行 wire
工具來生成代碼時(shí),它就會(huì)根據(jù)這些依賴關(guān)系來自動(dòng)創(chuàng)建和注入所需的實(shí)例。
注意:文件首行必須加上 //go:build wireinject
或 // +build wireinject
(go 1.18
之前的版本使用) 注釋,作用是只有在使用 wire
工具時(shí)才會(huì)編譯這部分代碼,其他情況下忽略。
接下來在 wire.go
文件所處目錄下執(zhí)行 wire
命令,生成 wire_gen.go
文件,內(nèi)容如下所示:
// Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject package wire import ( "chenmingyong0423/blog/tutorial-code/wire/internal/post/handler" "chenmingyong0423/blog/tutorial-code/wire/internal/post/service" "chenmingyong0423/blog/tutorial-code/wire/ioc" "github.com/gin-gonic/gin" ) // Injectors from wire.go: func InitializeApp() *gin.Engine { iPostService := service.NewPostService() postHandler := handler.NewPostHandler(iPostService) engine := ioc.NewGinEngineAndRegisterRoute(postHandler) return engine }
生成的代碼和我們手寫區(qū)別不大,當(dāng)我們的組件很多,依賴關(guān)系復(fù)雜的時(shí)候,我們才會(huì)感覺到 Wire
工具的好處。
Wire 的核心概念
Wire
有兩個(gè)核心概念:提供者(providers
)和注入器(injectors
)。
Wire 提供者(providers)
提供者:一個(gè)可以產(chǎn)生值的函數(shù),也就是有返回值的函數(shù)。例如入門代碼里的 NewPostHandler
函數(shù):
func NewPostHandler(serv service.IPostService) *PostHandler { return &PostHandler{serv: serv} }
返回值不僅限于一個(gè),如果有需要的話,可以額外添加一個(gè) error
的返回值。
如果提供者過多的時(shí)候,我們還可以以分組的形式進(jìn)行連接,例如將 post
相關(guān)的 handler
和 service
進(jìn)行組合:
package handler var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)
使用 wire.NewSet
函數(shù)將提供者進(jìn)行分組,該函數(shù)返回一個(gè) ProviderSet
結(jié)構(gòu)體。不僅如此,wire.NewSet
還能對(duì)多個(gè) ProviderSet
進(jìn)行分組 wire.NewSet(PostSet, XxxSet)
。
對(duì)于之前的 InitializeApp
函數(shù),我們可以這樣升級(jí):
//go:build wireinject package wire func InitializeAppV2() *gin.Engine { wire.Build( handler.PostSet, ioc.NewGinEngineAndRegisterRoute, ) return &gin.Engine{} }
然后通過 Wire
命令生成代碼,和之前的結(jié)果一致。
Wire 注入器(injectors)
注入器(injectors
)的作用是將所有的提供者(providers
)連接起來,回顧一下我們之前的代碼:
func InitializeApp() *gin.Engine { wire.Build( handler.NewPostHandler, service.NewPostService, ioc.NewGinEngineAndRegisterRoute, ) return &gin.Engine{} }
InitializeApp
函數(shù)就是一個(gè)注入器,函數(shù)內(nèi)部通過 wire.Build
函數(shù)連接所有的提供者,然后返回 &gin.Engine{}
,該返回值實(shí)際上并沒有使用到,只是為了滿足編譯器的要求,避免報(bào)錯(cuò)而已,真正的返回值來自 ioc.NewGinEngineAndRegisterRoute
。
Wire 的高級(jí)用法
綁定接口
回顧我們之前編寫的代碼:
package handler ··· func NewPostHandler(serv service.IPostService) *PostHandler { return &PostHandler{serv: serv} } ··· pakacge service ··· func NewPostService() IPostService { return &PostService{} } ···
NewPostHandler
函數(shù)依賴于 service.IPostService
接口,NewPostService
函數(shù)返回的是 IPostService
接口的值,這兩個(gè)地方的類型匹配,因此 Wire
工具能夠正確識(shí)別并生成代碼。然而,這并不是推薦的最佳實(shí)踐。因?yàn)樵?Go
中的 最佳實(shí)踐 是返回 具體的類型 的值,所以最好讓 NewPostService
返回具體類型 PostService
的值:
func NewPostServiceV2() *PostService { return &PostService{} }
但是這樣,Wire
工具將認(rèn)為 IPostService
接口類型與 PostService
類型不匹配,導(dǎo)致生成代碼失敗。因此我們需要修改注入器的代碼:
func InitializeAppV3() *gin.Engine { wire.Build( handler.NewPostHandler, service.NewPostServiceV2, ioc.NewGinEngineAndRegisterRoute, wire.Bind(new(service.IPostService), new(*service.PostService)), ) return &gin.Engine{} }
使用 wire.Bind
來建立接口類型和具體的實(shí)現(xiàn)類型之間的綁定關(guān)系,這樣 Wire
工具就可以根據(jù)這個(gè)綁定關(guān)系進(jìn)行類型匹配并生成代碼。
wire.Bind
函數(shù)的第一個(gè)參數(shù)是指向所需接口類型值的指針,第二個(gè)實(shí)參是指向?qū)崿F(xiàn)該接口的類型值的指針。
結(jié)構(gòu)體提供者(Struct Providers)
Wire
庫(kù)有一個(gè)函數(shù)是 wire.Struct
,它能根據(jù)現(xiàn)有的類型進(jìn)行構(gòu)造結(jié)構(gòu)體,我們來看看下面的例子:
package main type Name string func NewName() Name { return "陳明勇" } type PublicAccount string func NewPublicAccount() PublicAccount { return "公眾號(hào):Go技術(shù)干貨" } type User struct { MyName Name MyPublicAccount PublicAccount } func InitializeUser() *User { wire.Build( NewName, NewPublicAccount, wire.Struct(new(User), "MyName", "MyPublicAccount"), ) return &User{} }
上述代碼中,首先定義了自定義類型 Name
和 PublicAccount
以及結(jié)構(gòu)體類型 User
,并分別提供了 Name
和 PublicAccount
的初始化函數(shù)(providers
)。然后定義一個(gè)注入器(injectors
)InitializeUser
,用于構(gòu)造連接提供者并構(gòu)造 *User
實(shí)例。
使用 wire.Struct
函數(shù)需要傳遞兩個(gè)參數(shù),第一個(gè)參數(shù)是結(jié)構(gòu)體類型的指針值,另一個(gè)參數(shù)是一個(gè)可變參數(shù),表示需要注入的結(jié)構(gòu)體字段的名稱集。
根據(jù)上述代碼,使用 Wire
工具生成的代碼如下所示:
func InitializeUser() *User { name := NewName() publicAccount := NewPublicAccount() user := &User{ MyName: name, MyPublicAccount: publicAccount, } return user }
如果我們不想返回指針類型,只需要修改 InitializeUser
函數(shù)的返回值為非指針即可。
綁定值
有時(shí)候,我們可以在注入器中通過 值表達(dá)式 給一個(gè)類型進(jìn)行賦值,而不是依賴提供者(providers
)。
func InjectUser() User { wire.Build(wire.Value(User{MyName: "陳明勇"})) return User{} }
在上述代碼中,使用 wire.Value
函數(shù)通過表達(dá)式直接指定 MyName
的值,生成的代碼如下所示:
func InjectUser() User { user := _wireUserValue return user } var ( _wireUserValue = User{MyName: "陳明勇"} )
需要注意的是,值表達(dá)式將被復(fù)制到生成的代碼文件中。
對(duì)于接口類型,可以使用 InterfaceValue
:
func InjectPostService() service.IPostService { wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{})) return nil }
使用結(jié)構(gòu)體字段作為提供者(providers)
有些時(shí)候,你可以使用結(jié)構(gòu)體的某個(gè)字段作為提供者,從而生成一個(gè)類似 GetXXX
的函數(shù)。
func GetUserName() Name { wire.Build( NewUser, wire.FieldsOf(new(User), "MyName"), ) return "" }
你可以使用 wire.FieldsOf
函數(shù)添加任意字段,生成的代碼如下所示:
func GetUserName() Name { user := NewUser() name := user.MyName return name } func NewUser() User { return User{MyName: Name("陳明勇"), MyPublicAccount: PublicAccount("公眾號(hào):Go技術(shù)干貨")} }
清理函數(shù)
如果一個(gè)提供者創(chuàng)建了一個(gè)需要清理的值(例如關(guān)閉一個(gè)文件),那么它可以返回一個(gè)閉包來清理資源。注入器會(huì)用它來給調(diào)用者返回一個(gè)聚合的清理函數(shù),或者在注入器實(shí)現(xiàn)中稍后調(diào)用的提供商返回錯(cuò)誤時(shí)清理資源。
func provideFile(log Logger, path Path) (*os.File, func(), error) { f, err := os.Open(string(path)) if err != nil { return nil, nil, err } cleanup := func() { if err := f.Close(); err != nil { log.Log(err) } } return f, cleanup, nil }
備用注入器語(yǔ)法
如果你不喜歡將類似這種寫法 → return &gin.Engine{}
放在你的注入器函數(shù)聲明的末尾,你可以用 panic
來更簡(jiǎn)潔地寫它:
func InitializeGin() *gin.Engine { panic(wire.Build(/* ... */)) }
小結(jié)
在本文中,我們?cè)敿?xì)探討了 Go Wire
工具的基本用法和高級(jí)特性。它是一個(gè)專為依賴注入設(shè)計(jì)的代碼生成工具,它不僅提供了基礎(chǔ)的依賴解析和代碼生成功能,還支持多種高級(jí)用法,如接口綁定和構(gòu)造結(jié)構(gòu)體。
依賴注入的設(shè)計(jì)模式應(yīng)用非常廣泛,Wire
工具讓依賴注入在 Go
語(yǔ)言中變得更簡(jiǎn)單。
以上就是深入淺出go依賴注入工具Wire的使用的詳細(xì)內(nèi)容,更多關(guān)于go依賴注入工具Wire的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang 中的可測(cè)試示例函數(shù)(Example Function)詳解
這篇文章詳細(xì)講解了 Golang 中的可測(cè)試示例函數(shù),示例函數(shù)類似于單元測(cè)試函數(shù),但沒有 *testing 類型的參數(shù),編寫示例函數(shù)也是很容易的,本文就通過代碼示例給大家介紹一下Golang的可測(cè)試示例函數(shù),需要的朋友可以參考下2023-07-07Go泛型實(shí)戰(zhàn)教程之如何在結(jié)構(gòu)體中使用泛型
這篇文章主要介紹了Go泛型實(shí)戰(zhàn)教程之如何在結(jié)構(gòu)體中使用泛型,根據(jù)Go泛型使用的三步曲提到的:類型參數(shù)化、定義類型約束、類型實(shí)例化我們一步步來定義我們的緩存結(jié)構(gòu)體,需要的朋友可以參考下2022-07-07Go切片導(dǎo)致rand.Shuffle產(chǎn)生重復(fù)數(shù)據(jù)的原因與解決方案
在 Go 語(yǔ)言的實(shí)際開發(fā)中,切片(slice)是一種非常靈活的數(shù)據(jù)結(jié)構(gòu),然而,由于其底層數(shù)據(jù)共享的特性,在某些情況下可能會(huì)導(dǎo)致意想不到的 Bug,本文將詳細(xì)分析 rand.Shuffle 之后,切片中的數(shù)據(jù)出現(xiàn)重復(fù)的問題,探討其根本原因,并給出最佳解決方案,需要的朋友可以參考下2025-02-02go?mod?tidy報(bào)錯(cuò):zip:?not?a?valid?zip?file解決辦法
這篇文章主要給大家介紹了關(guān)于go?mod?tidy報(bào)錯(cuò):zip:?not?a?valid?zip?file的解決辦法,go mod是進(jìn)行代碼管理,這錯(cuò)誤是因?yàn)楸镜胤种Ш瓦h(yuǎn)程分支沖突,本文通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01使用gRPC實(shí)現(xiàn)獲取數(shù)據(jù)庫(kù)版本
這篇文章主要為大家詳細(xì)介紹了如何使用gRPC實(shí)現(xiàn)獲取數(shù)據(jù)庫(kù)版本,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12如何使用Golang創(chuàng)建與讀取Excel文件
我最近工作忙于作圖,圖表,需要自己準(zhǔn)備數(shù)據(jù)源,所以經(jīng)常和Excel打交道,下面這篇文章主要給大家介紹了關(guān)于如何使用Golang創(chuàng)建與讀取Excel文件的相關(guān)資料,需要的朋友可以參考下2022-07-07