目录
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 }
GetxxxGetxxx
不仅如此,我们的依赖都是写死的,即依赖者的代码中写死了被依赖者的生成关系。当被依赖者的生成方式改变,我们也需要改变依赖者的函数,这极大的增加了修改代码量以及出错风险。
接下来我们用依赖注入的方式对代码进行改造:
// 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/binwire$GOPATH/bin$PATHwire
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
1.提取一个 init 方法 InitializeBroadCast:
func main() { b := demo.InitializeBroadCast() b.Start() }
2.编写一个 wire.go 文件,用于 wire 工具来解析依赖,生成代码:
//+build wireinject package demo func InitializeBroadCast() BroadCast { wire.Build(NewBroadCast, NewChannel, NewMessage) return BroadCast{} }
注意:需要在文件头部增加构建约束://+build wireinject
wire gen wire.go
// 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 }
wireinitNewBroadCastNewChannelNewMessagewire
wire.gowire_gen.go+buildwireinject!wireinject+buildgo buildwirewireinjectwire.gowire_gen.gowire!wireinject
3.3 基础概念
WireProviderInjector
ProviderNewBroadCastProviderInjectorProvidersProvidersInitializeBroadCastInjector
4. Wire使用实践
wire
projectBuildInjector
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
// 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 }
app.BuildInjector
injector, err := BuildInjector() if err != nil { return nil, err } //project服务启动 svr := projectservice.NewServer(injector.ProjectHandler, logOpt) svr.Run()
BuildInjector//+build wireinjectpackage app
4.2 高级特性
4.2.1 NewSet
NewSetInjectorNewSetProviderSet
// 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
ProviderProviderWire
// 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
BindWireWireBind
// 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
Injectorfunc()ProviderInjectorWireProvider
- 第一个返回值是需要生成的对象
- 如果有 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()
4.3 高阶使用
wireproject
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 的介绍请传送至:https://github.com/google/wire