Go 語言官方依賴注入工具 Wire 使用指北
精選1. 前言
接觸 Golang 有一段時間了,發(fā)現(xiàn) Golang 同樣需要類似 Java 中 Spring 一樣的依賴注入框架。如果項目規(guī)模比較小,是否有依賴注入框架問題不大,但當(dāng)項目變大之后,有一個合適的依賴注入框架是十分必要的。通過調(diào)研,了解到 Golang 中常用的依賴注入工具主要有 Inject 、Dig 等。但是今天主要介紹的是 Go 團(tuán)隊開發(fā)的 Wire,一個編譯期實現(xiàn)依賴注入的工具。
2. 依賴注入(DI)是什么
說起依賴注入?就要引出另一個名詞控制反轉(zhuǎn)?( IoC )。IoC 是一種設(shè)計思想,其核心作用是降低代碼的耦合度。依賴注入?是一種實現(xiàn)控制反轉(zhuǎn)且用于解決依賴性問題的設(shè)計模式。
舉個例子,假設(shè)我們代碼分層關(guān)系是 dal 層連接數(shù)據(jù)庫,負(fù)責(zé)數(shù)據(jù)庫的讀寫操作。那么我們的 dal 層的上一層 service 負(fù)責(zé)調(diào)用 dal 層處理數(shù)據(jù),在我們目前的代碼中,它可能是這樣的:
// 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
}
在這段代碼里,層級依賴關(guān)系為 service -> dal -> db,上游層級通過 Getxxx?實例化依賴。但在實際生產(chǎn)中,我們的依賴鏈比較少是垂直依賴關(guān)系,更多的是橫向依賴。即我們一個方法中,可能要多次調(diào)用Getxxx的方法,這樣使得我們代碼極不簡潔。
不僅如此,我們的依賴都是寫死的,即依賴者的代碼中寫死了被依賴者的生成關(guān)系。當(dāng)被依賴者的生成方式改變,我們也需要改變依賴者的函數(shù),這極大的增加了修改代碼量以及出錯風(fēng)險。
接下來我們用依賴注入的方式對代碼進(jìn)行改造:
// 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 中,實現(xiàn)了層級間的依賴注入。解耦了部分依賴關(guān)系。
在系統(tǒng)簡單、代碼量少的情況下上面的實現(xiàn)方式確實沒什么問題。但是項目龐大到一定程度,結(jié)構(gòu)之間的關(guān)系變得非常復(fù)雜時,手動創(chuàng)建每個依賴,然后層層組裝起來的方式就會變得異常繁瑣,并且容易出錯。這個時候勇士 wire 出現(xiàn)了!
3. Wire Come
3.1 簡介
Wire 是一個輕巧的 Golang 依賴注入工具。它由 Go Cloud 團(tuán)隊開發(fā),通過自動生成代碼的方式在編譯期完成依賴注入。它不需要反射機(jī)制,后面會看到, 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。
現(xiàn)在我們有這樣的三個類型:
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}
}
假設(shè) Channel 有一個 GetMsg 方法,BroadCast 有一個 Start 方法:
func (c Channel) GetMsg() Message {
return c.Message
}
func (b BroadCast) Start() {
msg := b.Channel.GetMsg()
fmt.Println(msg)
}
如果手動寫代碼的話,我們的寫法應(yīng)該是:
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{}
}
注意:需要在文件頭部增加構(gòu)建約束://+build wireinject
- 使用 wire 工具,生成代碼,在 wire.go 所在目錄下執(zhí)行命令:wire gen wire.go。會生成如下代碼,即在編譯代碼時真正使用的Init函數(shù):
// 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 工具會根據(jù)這些方法的函數(shù)簽名(參數(shù)類型/返回值類型/函數(shù)名)自動推導(dǎo)依賴關(guān)系。
wire.go? 和 wire_gen.go? 文件頭部位置都有一個 +build?,不過一個后面是 wireinject?,另一個是 !wireinject。+build? 其實是 Go 語言的一個特性。類似 C/C++ 的條件編譯,在執(zhí)行 go build? 時可傳入一些選項,根據(jù)這個選項決定某些文件是否編譯。wire? 工具只會處理有wireinject? 的文件,所以我們的 wire.go? 文件要加上這個。生成的 wire_gen.go? 是給我們來使用的,wire? 不需要處理,故有 !wireinject。
3.3 基礎(chǔ)概念
Wire? 有兩個基礎(chǔ)概念,Provider?(構(gòu)造器)和 Injector(注入器)
- Provider? 實際上就是生成組件的普通方法,這些方法接收所需依賴作為參數(shù),創(chuàng)建組件并將其返回。我們上面例子的 NewBroadCast 就是 Provider。
- Injector? 可以理解為 Providers 的連接器,它用來按依賴順序調(diào)用 Providers 并最終返回構(gòu)建目標(biāo)。我們上面例子的 InitializeBroadCast 就是 Injector。
4. Wire使用實踐
下面簡單介紹一下 wire 在飛書問卷表單服務(wù)中的應(yīng)用。
飛書問卷表單服務(wù)的 project? 模塊中將 handler 層、service 層和 dal 層的初始化通過參數(shù)注入的方式實現(xiàn)依賴反轉(zhuǎn)。通過 BuildInjector 注入器來初始化所有的外部依賴。
4.1 基礎(chǔ)使用
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服務(wù)啟動
svr := projectservice.NewServer(injector.ProjectHandler, logOpt)
svr.Run()
注意,如果你運行時,出現(xiàn)了 BuildInjector? 重定義,那么檢查一下你的 //+build wireinject? 與 package app 這兩行之間是否有空行,這個空行必須要有!見https://github.com/google/wire/issues/117
4.2 高級特性
4.2.1 NewSet
NewSet? 一般應(yīng)用在初始化對象比較多的情況下,減少 Injector? 里面的信息。當(dāng)我們項目龐大到一定程度時,可以想象會出現(xiàn)非常多的 Providers。NewSet? 幫我們把這些 Providers 按照業(yè)務(wù)關(guān)系進(jìn)行分組,組成 ProviderSet(構(gòu)造器集合),后續(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? 都是函數(shù),除函數(shù)外,結(jié)構(gòu)體也可以充當(dāng) Provider? 的角色。Wire 給我們提供了結(jié)構(gòu)構(gòu)造器(Struct Provider)。結(jié)構(gòu)構(gòu)造器創(chuàng)建某個類型的結(jié)構(gòu),然后用參數(shù)或調(diào)用其它構(gòu)造器填充它的字段。
// project_service.go
// 函數(shù)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? 函數(shù)的作用是為了讓接口類型的依賴參與 Wire? 的構(gòu)建。Wire? 的構(gòu)建依靠參數(shù)類型,接口類型是不支持的。Bind 函數(shù)通過將接口類型和實現(xiàn)類型綁定,來達(dá)到依賴注入的目的。
// 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
構(gòu)造器可以提供一個清理函數(shù)(cleanup),如果后續(xù)的構(gòu)造器返回失敗,前面構(gòu)造器返回的清理函數(shù)都會調(diào)用。初始化 Injector? 之后可以獲取到這個清理函數(shù),清理函數(shù)典型的應(yīng)用場景是文件資源和網(wǎng)絡(luò)連接資源。清理函數(shù)通常作為第二返回值,參數(shù)類型為 func()?。當(dāng) Provider? 中的任何一個擁有清理函數(shù),Injector? 的函數(shù)返回值中也必須包含該函數(shù)。并且 Wire? 對 Provider 的返回值個數(shù)及順序有以下限制:
- 第一個返回值是需要生成的對象
- 如果有 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的清理函數(shù)都會在這里
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 服務(wù)進(jìn)行代碼改造:
project_dal.go
type IProjectDal interface {
Create(ctx context.Context, item *entity.Project) (err error)
// ...
}
type ProjectDal struct {
DB *gorm.DB
}
// wire.Struct方法是wire提供的構(gòu)造器,"*"代表為所有字段注入值,在這里可以用"DB"代替
// wire.Bind方法把接口和實現(xiàn)綁定起來
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 官方認(rèn)為這種情況,是設(shè)計上的缺陷。這種情況下,可以通過類型別名來將對象的類型進(jìn)行區(qū)分。
例如服務(wù)會同時操作兩個 Redis 實例,RedisA & RedisB
func NewRedisA() *goredis.Client {...}
func NewRedisB() *goredis.Client {...}
對于這種情況,wire 無法推導(dǎo)依賴的關(guān)系??梢赃@樣進(jìn)行實現(xiàn):
type RedisCliA *goredis.Client
type RedisCliB *goredis.Client
func NewRedisA() RedicCliA {...}
func NewRedisB() RedicCliB {...}
5.2 單例問題
依賴注入的本質(zhì)是用單例來綁定接口和實現(xiàn)接口對象間的映射關(guān)系。而通常實踐中不可避免的有些對象是有狀態(tài)的,同一類型的對象總是要在不同的用例場景發(fā)生變化,單例就會引起數(shù)據(jù)的錯誤,不能保存彼此的狀態(tài)。針對這種場景我們通常設(shè)計多層的 DI 容器來實現(xiàn)單例隔離,亦或是脫離 DI 容器自行管理對象的生命周期。
6. 結(jié)語
Wire 是一個強(qiáng)大的依賴注入工具。與 Inject 、Dig 等不同的是,Wire只生成代碼而不是使用反射在運行時注入,不用擔(dān)心會有性能損耗。項目工程化過程中,Wire 可以很好協(xié)助我們完成復(fù)雜對象的構(gòu)建組裝。
更多關(guān)于 Wire 的介紹請傳送至:https://github.com/google/wire