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(ctxcontext.Context,data*UserCreateParams)error{
db:=mysql.GetDB().Model(&entity.User{})
user:=entity.User{
Username:data.Username,
Password:data.Password,
}
returndb.Create(&user).Error
}
//service/user.go
func(u*UserService)Register(ctxcontext.Context,data*schema.RegisterReq)(*schema.RegisterRes,error){
params:=dal.UserCreateParams{
Username:data.Username,
Password:data.Password,
}
err:=dal.GetUserDal().Create(ctx,params)
iferr!=nil{
returnnil,err
}
registerRes:=schema.RegisterRes{
Msg:"registersuccess",
}
return&registerRes,nil
}

在这段代码里,层级依靠联系为 service -> dal -> db,上游层级经过Getxxx实例化依靠。但在实践生产中,咱们的依靠链比较少是垂直依靠联系,更多的是横向依靠。即咱们一个办法中,可能要多次调用Getxxx的办法,这样使得咱们代码极不简洁。

不仅如此,咱们的依靠都是写死的,即依靠者的代码中写死了被依靠者的生成联系。当被依靠者的生成办法改动,咱们也需求改动依靠者的函数,这极大的增加了修正代码量以及出错风险。

接下来咱们用依靠注入的办法对代码进行改造:

//dal/user.go
typeUserDalstruct{
DB*gorm.DB
}
funcNewUserDal(db*gorm.DB)*UserDal{
return&UserDal{
DB:db
}
}
func(u*UserDal)Create(ctxcontext.Context,data*UserCreateParams)error{
db:=u.DB.Model(&entity.User{})
user:=entity.User{
Username:data.Username,
Password:data.Password,
}
returndb.Create(&user).Error
}
//service/user.go
typeUserServicestruct{
UserDal*dal.UserDal
}
funcNewUserService(userDaldal.UserDal)*UserService{
return&UserService{
UserDal:userDal
}
}
func(u*UserService)Register(ctxcontext.Context,data*schema.RegisterReq)(*schema.RegisterRes,error){
params:=dal.UserCreateParams{
Username:data.Username,
Password:data.Password,
}
err:=u.UserDal.Create(ctx,params)
iferr!=nil{
returnnil,err
}
registerRes:=schema.RegisterRes{
Msg:"registersuccess",
}
return&registerRes,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 的安装:

gogetgithub.com/google/wire/cmd/wire

上面的指令会在$GOPATH/bin中生成一个可履行程序wire,这便是代码生成器。能够把$GOPATH/bin参加体系环境变量$PATH中,所以可直接在指令行中履行wire指令。

下面咱们在一个比如中看看如何运用wire

现在咱们有这样的三个类型:

typeMessagestring
typeChannelstruct{
MessageMessage
}
typeBroadCaststruct{
ChannelChannel
}

三者的 init 办法:

funcNewMessage()Message{
returnMessage("HelloWire!")
}
funcNewChannel(mMessage)Channel{
returnChannel{Message:m}
}
funcNewBroadCast(cChannel)BroadCast{
returnBroadCast{Channel:c}
}

假定 Channel 有一个 GetMsg 办法,BroadCast 有一个 Start 办法:

func(cChannel)GetMsg()Message{
returnc.Message
}
func(bBroadCast)Start(){
msg:=b.Channel.GetMsg()
fmt.Println(msg)
}

假如手动写代码的话,咱们的写法应该是:

funcmain(){
message:=NewMessage()
channel:=NewChannel(message)
broadCast:=NewBroadCast(channel)
broadCast.Start()
}

假如运用wire,咱们需求做的就变成如下的工作了:

  1. 提取一个 init 办法 InitializeBroadCast:
funcmain(){
b:=demo.InitializeBroadCast()
b.Start()
}
  1. 编写一个 wire.go 文件,用于 wire 东西来解析依靠,生成代码:
//+buildwireinject
packagedemo
funcInitializeBroadCast()BroadCast{
wire.Build(NewBroadCast,NewChannel,NewMessage)
returnBroadCast{}
}

留意:需求在文件头部增加构建约束://+build wireinject

  1. 运用 wire 东西,生成代码,在 wire.go 地点目录下履行指令:wire gen wire.go。会生成如下代码,即在编译代码时真实运用的Init函数:
//CodegeneratedbyWire.DONOTEDIT.
//go:generatewire
//+build!wireinject
funcInitializeBroadCast()BroadCast{
message:=NewMessage()
channel:=NewChannel(message)
broadCast:=NewBroadCast(channel)
returnbroadCast
}

咱们告知wire,咱们所用到的各种组件的init办法(NewBroadCast,NewChannel,NewMessage),那么wire东西会依据这些办法的函数签名(参数类型/回来值类型/函数名)自动推导依靠联系。

wire.gowire_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 伪代码如下:

funcNewProjectDal(db*gorm.DB)*ProjectDal{
return&ProjectDal{
DB:db
}
}
typeProjectDalstruct{
DB*gorm.DB
}
func(dal*ProjectDal)Create(ctxcontext.Context,item*entity.Project)error{
result:=dal.DB.Create(item)
returnerrors.WithStack(result.Error)
}
//QuestionDal、QuestionModelDal...

service 伪代码如下:

funcNewProjectService(projectDal*dal.ProjectDal,questionDal*dal.QuestionDal,questionModelDal*dal.QuestionModelDal)*ProjectService{
return&projectService{
ProjectDal:projectDal,
QuestionDal:questionDal,
QuestionModelDal:questionModelDal,
}
}
typeProjectServicestruct{
ProjectDal*dal.ProjectDal
QuestionDal*dal.QuestionDal
QuestionModelDal*dal.QuestionModelDal
}
func(s*ProjectService)Create(ctxcontext.Context,projectBo*bo.ProjectCreateBo)(int64,error){}

handler 伪代码如下:

funcNewProjectHandler(srv*service.ProjectService)*ProjectHandler{
return&ProjectHandler{
ProjectService:srv
}
}
typeProjectHandlerstruct{
ProjectService*service.ProjectService
}
func(s*ProjectHandler)CreateProject(ctxcontext.Context,req*project.CreateProjectRequest)(resp*
project.CreateProjectResponse,errerror){}

injector.go 伪代码如下:

funcNewInjector()(handler*handler.ProjectHandler)*Injector{
return&Injector{
ProjectHandler:handler
}
}
typeInjectorstruct{
ProjectHandler*handler.ProjectHandler
//components,others...
}

在 wire.go 中如下界说:

//+buildwireinject
packageapp
funcBuildInjector()(*Injector,error){
wire.Build(
NewInjector,
//handler
handler.NewProjectHandler,
//services
service.NewProjectService,
//更多service...
//dal
dal.NewProjectDal,
dal.NewQuestionDal,
dal.NewQuestionModelDal,
//更多dal...
//db
common.InitGormDB,
//othercomponents...
)
returnnew(Injector),nil
}

履行wire gen ./internal/app/wire.go生成 wire_gen.go

//CodegeneratedbyWire.DONOTEDIT.
//go:generatewire
//+build!wireinject
funcBuildInjector()(*Injector,error){
db,err:=common.InitGormDB()
iferr!=nil{
returnnil,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)
returninjector,nil
}

在 main.go 中参加初始化 injector 的办法app.BuildInjector

injector,err:=BuildInjector()
iferr!=nil{
returnnil,err
}
//project服务启动
svr:=projectservice.NewServer(injector.ProjectHandler,logOpt)
svr.Run()

留意,假如你运行时,呈现了BuildInjector重界说,那么检查一下你的//+build wireinjectpackage app这两行之间是否有空行,这个空行有必要要有!见github.com/google/wire…

4.2 高档特性

4.2.1 NewSet

NewSet一般应用在初始化目标比较多的状况下,减少Injector里边的信息。当咱们项目庞大到必定程度时,能够想象会呈现非常多的 Providers。NewSet帮咱们把这些 Providers 按照业务联系进行分组,组成ProviderSet(结构器调集),后续只需求运用这个调集即可。

//project.go
varProjectSet=wire.NewSet(NewProjectHandler,NewProjectService,NewProjectDal)
//wire.go
funcBuildInjector()(*Injector,error){
wire.Build(InitGormDB,ProjectSet,NewInjector)
returnnew(Injector),nil
}

4.2.2 Struct

上述比如的Provider都是函数,除函数外,结构体也能够充任Provider的人物。Wire给咱们供给了结构结构器(Struct Provider)。结构结构器创立某个类型的结构,然后用参数或调用其它结构器填充它的字段。

//project_service.go
//函数provider
funcNewProjectService(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界说:
typeAppstruct{
Foo*Foo
Bar*Bar
NoInjectint`wire:"-"`
}

4.2.3 Bind

Bind函数的效果是为了让接口类型的依靠参与Wire的构建。Wire的构建依靠参数类型,接口类型是不支持的。Bind函数经过将接口类型和完结类型绑定,来到达依靠注入的意图。

//project_dal.go
typeIProjectDalinterface{
Create(ctxcontext.Context,item*entity.Project)(errerror)
//...
}
typeProjectDalstruct{
DB*gorm.DB
}
varbind=wire.Bind(new(IProjectDal),new(*ProjectDal))

4.2.4 CleanUp

结构器能够供给一个整理函数(cleanup),假如后续的结构器回来失利,前面结构器回来的整理函数都会调用。初始化Injector之后能够获取到这个整理函数,整理函数典型的应用场景是文件资源和网络衔接资源。整理函数一般作为第二回来值,参数类型为func()。当Provider中的任何一个具有整理函数,Injector的函数回来值中也有必要包含该函数。而且WireProvider的回来值个数及顺序有以下限制:

  1. 第一个回来值是需求生成的目标
  2. 假如有 2 个回来值,第二个回来值有必要是 func() 或 error
  3. 假如有 3 个回来值,第二个回来值有必要是 func(),而第三个回来值有必要是 error
//db.go
funcInitGormDB()(*gorm.DB,func(),error){
//初始化db链接
//...
cleanFunc:=func(){
db.Close()
}
returndb,cleanFunc,nil
}
//wire.go
funcBuildInjector()(*Injector,func(),error){
wire.Build(
common.InitGormDB,
//...
NewInjector
)
returnnew(Injector),nil,nil
}
//生成的wire_gen.go
funcBuildInjector()(*Injector,func(),error){
db,cleanup,err:=common.InitGormDB()
//...
returninjector,func(){
//一切provider的整理函数都会在这里
cleanup()
},nil
}
//main.go
injector,cleanFunc,err:=app.BuildInjector()
defercleanFunc()

更多用法具体能够参考 wire官方指南:github.com/google/wire…

4.3 高阶运用

接着咱们就用上述的这些wire高档特性对project服务进行代码改造:

project_dal.go

typeIProjectDalinterface{
Create(ctxcontext.Context,item*entity.Project)(errerror)
//...
}
typeProjectDalstruct{
DB*gorm.DB
}
//wire.Struct办法是wire供给的结构器,"*"代表为一切字段注入值,在这里能够用"DB"替代
//wire.Bind办法把接口和完结绑定起来
varProjectSet=wire.NewSet(
wire.Struct(new(ProjectDal),"*"),
wire.Bind(new(IProjectDal),new(*ProjectDal)))
func(dal*ProjectDal)Create(ctxcontext.Context,item*entity.Project)error{}

dal.go

//DalSetdal注入
varDalSet=wire.NewSet(
ProjectSet,
//QuestionDalSet、QuestionModelDalSet...
)

project_service.go

typeIProjectServiceinterface{
Create(ctxcontext.Context,projectBo*bo.CreateProjectBo)(int64,error)
//...
}
typeProjectServicestruct{
ProjectDaldal.IProjectDal
QuestionDaldal.IQuestionDal
QuestionModelDaldal.IQuestionModelDal
}
func(s*ProjectService)Create(ctxcontext.Context,projectBo*bo.ProjectCreateBo)(int64,error){}
varProjectSet=wire.NewSet(
wire.Struct(new(ProjectService),"*"),
wire.Bind(new(IProjectService),new(*ProjectService)))

service.go

//ServiceSetservice注入
varServiceSet=wire.NewSet(
ProjectSet,
//otherserviceset...
)

handler 伪代码如下:

varProjectHandlerSet=wire.NewSet(wire.Struct(new(ProjectHandler),"*"))
typeProjectHandlerstruct{
ProjectServiceservice.IProjectService
}
func(s*ProjectHandler)CreateProject(ctxcontext.Context,req*project.CreateProjectRequest)(resp*
project.CreateProjectResponse,errerror){}

injector.go 伪代码如下:

varInjectorSet=wire.NewSet(wire.Struct(new(Injector),"*"))
typeInjectorstruct{
ProjectHandler*handler.ProjectHandler
//others...
}

wire.go

//+buildwireinject
packageapp
funcBuildInjector()(*Injector,func(),error){
wire.Build(
//db
common.InitGormDB,
//dal
dal.DalSet,
//services
service.ServiceSet,
//handler
handler.ProjectHandlerSet,
//injector
InjectorSet,
//othercomponents...
)
returnnew(Injector),nil,nil
}

5. 留意事项

5.1 相同类型问题

wire 不允许不同的注入目标具有相同的类型。google 官方认为这种状况,是规划上的缺陷。这种状况下,能够经过类型别号来将目标的类型进行区分。

例如服务会一起操作两个 Redis 实例,RedisA & RedisB

funcNewRedisA()*goredis.Client{...}
funcNewRedisB()*goredis.Client{...}

关于这种状况,wire 无法推导依靠的联系。能够这样进行完结:

typeRedisCliA*goredis.Client
typeRedisCliB*goredis.Client
funcNewRedisA()RedicCliA{...}
funcNewRedisB()RedicCliB{...}

5.2 单例问题

依靠注入的实质是用单例来绑定接口和完结接口目标间的映射联系。而一般实践中不可避免的有些目标是有状况的,同一类型的目标总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存互相的状况。针对这种场景咱们一般规划多层的 DI 容器来完结单例阻隔,亦或是脱离 DI 容器自行办理目标的生命周期。

6. 结语

Wire 是一个强壮的依靠注入东西。与 Inject 、Dig 等不同的是,Wire只生成代码而不是运用反射在运行时注入,不用担心会有功能损耗。项目工程化过程中,Wire 能够很好协助咱们完结杂乱目标的构建拼装。

更多关于 Wire 的介绍请传送至:github.com/google/wire

7. 关于咱们

咱们来自字节跳动飞书商业应用研发部(Lark Business Applications),现在咱们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了工作区域。咱们重视的产品范畴首要在企业经历办理软件上,包含飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 范畴体系,也包含飞书批阅、OA、法务、财务、收购、差旅与报销等体系。

欢迎参加咱们。扫码发现职位&投递简历(二维码如下)官网投递:

Go 语言官方依赖注入工具 Wire 使用指北