目录

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 &registerRes, 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 &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 的安装:

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

您可能感兴趣的文章: