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

