Go 語言官方依賴注入工具 Wire 使用指北
1. 前言
接觸 Golang 有一段時間了,發現 Golang 同樣需要類似 Java 中 Spring 一樣的依賴注入框架。如果專案規模比較小,是否有依賴注入框架問題不大,但當專案變大之後,有一個合適的依賴注入框架是十分必要的。通過調研,瞭解到 Golang 中常用的依賴注入工具主要有 Inject 、Dig 等。但是今天主要介紹的是 Go 團隊開發的 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 中,實現了層級間的依賴注入。解耦了部分依賴關係。
在系統簡單、程式碼量少的情況下上面的實現方式確實沒什麼問題。但是專案龐大到一定程度,結構之間的關係變得非常複雜時,手動建立每個依賴,然後層層組裝起來的方式就會變得異常繁瑣,並且容易出錯。這個時候勇士 wire 出現了!
3. Wire Come
3.1 簡介
Wire 是一個輕巧的 Golang 依賴注入工具。它由 Go Cloud 團隊開發,通過自動生成程式碼的方式在編譯期完成依賴注入。它不需要反射機制,後面會看到, Wire 生成的程式碼與手寫無異。
3.2 快速使用
wire 的安裝:
go get github.com/google/wire/cmd/wire
上面的命令會在 $GOPATH/bin
中生成一個可執行程式 wire
,這就是程式碼生成器。可以把$GOPATH/bin
加入系統環境變數 $PATH
中,所以可直接在命令列中執行 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
,我們需要做的就變成如下的工作了:
- 提取一個 init 方法 InitializeBroadCast:
``` func main() { b := demo.InitializeBroadCast()
b.Start() } ```
- 編寫一個 wire.go 檔案,用於 wire 工具來解析依賴,生成程式碼:
``` //+build wireinject
package demo
func InitializeBroadCast() BroadCast { wire.Build(NewBroadCast, NewChannel, NewMessage) return BroadCast{} } ```
注意:需要在檔案頭部增加構建約束://+build wireinject
- 使用 wire 工具,生成程式碼,在 wire.go 所在目錄下執行命令:
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++ 的條件編譯,在執行 go build
時可傳入一些選項,根據這個選項決定某些檔案是否編譯。wire
工具只會處理有wireinject
的檔案,所以我們的 wire.go
檔案要加上這個。生成的 wire_gen.go
是給我們來使用的,wire
不需要處理,故有 !wireinject
。
3.3 基礎概念
Wire
有兩個基礎概念,Provider
(構造器)和 Injector
(注入器)
Provider
實際上就是生成元件的普通方法,這些方法接收所需依賴作為引數,建立元件並將其返回。我們上面例子的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 } ```
執行 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
這兩行之間是否有空行,這個空行必須要有!見http://github.com/google/wire/issues/117
4.2 高階特性
4.2.1 NewSet
NewSet
一般應用在初始化物件比較多的情況下,減少 Injector
裡面的資訊。當我們專案龐大到一定程度時,可以想象會出現非常多的 Providers。NewSet
幫我們把這些 Providers 按照業務關係進行分組,組成 ProviderSet
(構造器集合),後續只需要使用這個集合即可。
``` // 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)。結構構造器建立某個型別的結構,然後用引數或呼叫其它構造器填充它的欄位。
``` // 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),如果後續的構造器返回失敗,前面構造器返回的清理函式都會呼叫。初始化 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官方指南:http://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 官方認為這種情況,是設計上的缺陷。這種情況下,可以通過類型別名來將物件的型別進行區分。
例如服務會同時操作兩個 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 單例問題
依賴注入的本質是用單例來繫結介面和實現介面物件間的對映關係。而通常實踐中不可避免的有些物件是有狀態的,同一型別的物件總是要在不同的用例場景發生變化,單例就會引起資料的錯誤,不能儲存彼此的狀態。針對這種場景我們通常設計多層的 DI 容器來實現單例隔離,亦或是脫離 DI 容器自行管理物件的生命週期。
6. 結語
Wire 是一個強大的依賴注入工具。與 Inject 、Dig 等不同的是,Wire只生成程式碼而不是使用反射在執行時注入,不用擔心會有效能損耗。專案工程化過程中,Wire 可以很好協助我們完成複雜物件的構建組裝。
更多關於 Wire 的介紹請傳送至:http://github.com/google/wire
7. 關於我們
我們來自位元組跳動飛書商業應用研發部(Lark Business Applications),目前我們在北京、深圳、上海、武漢、杭州、成都、廣州、三亞都設立了辦公區域。我們關注的產品領域主要在企業經驗管理軟體上,包括飛書 OKR、飛書績效、飛書招聘、飛書人事等 HCM 領域系統,也包括飛書審批、OA、法務、財務、採購、差旅與報銷等系統。
歡迎加入我們。掃碼發現職位&投遞簡歷(二維碼如下)官網投遞:
- 解鎖抖音世界盃的畫質優化實踐
- Kafka 架構、核心機制和場景解讀
- 頭條穩定性治理:ARC 環境中對 Objective-C 物件賦值的 Crash 隱患
- 位元組跳動模型大規模部署實戰
- 「飛書績效」寬表SQL自動生成邏輯淺析
- Mybatis原始碼主流程分析
- 推薦系統的Bias
- 抖音 Android 基礎技術大揭祕!| 位元組跳動技術沙龍第十期
- 基於序列標註模型的主動學習實踐
- 加密技術科普
- 二維碼掃描優化
- 前端監控系列4 | SDK 體積與效能優化實踐
- 特效側使用者體驗優化實戰 —— 包體積篇
- 深入理解 Android Studio Sync 流程
- 選擇 Go 還是 Rust?CloudWeGo-Volo 基於 Rust 語言的探索實踐
- 初探自然語言預訓練技術演進之路
- 高效能 RPC 框架 CloudWeGo-Kitex 內外統一的開源實踐
- 開源 1 週年突破 1w Star - CloudWeGo 開源社群實踐分享
- Go 語言官方依賴注入工具 Wire 使用指北
- prompt 綜述