6

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

 1 year ago
source link: https://juejin.cn/post/7146852457774055437
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

2022年09月24日 07:57 ·  阅读 120
Go 语言官方依赖注入工具 Wire 使用指北

接触 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
}
复制代码

在这段代码里,层级依赖关系为 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 &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/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,我们需要做的就变成如下的工作了:

  1. 提取一个 init 方法 InitializeBroadCast:
func main() {
    b := demo.InitializeBroadCast()

    b.Start()
}
复制代码
  1. 编写一个 wire.go 文件,用于 wire 工具来解析依赖,生成代码:
//+build wireinject

package demo

func InitializeBroadCast() BroadCast {
    wire.Build(NewBroadCast, NewChannel, NewMessage)
    return BroadCast{}
}
复制代码

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

  1. 使用 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 方法(NewBroadCastNewChannelNewMessage),那么 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 这两行之间是否有空行,这个空行必须要有!见github.com/google/wire…

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 的返回值个数及顺序有以下限制:

  1. 第一个返回值是需要生成的对象
  2. 如果有 2 个返回值,第二个返回值必须是 func() 或 error
  3. 如果有 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官方指南:github.com/google/wire…

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 容器自行管理对象的生命周期。

Wire 是一个强大的依赖注入工具。与 Inject 、Dig 等不同的是,Wire只生成代码而不是使用反射在运行时注入,不用担心会有性能损耗。项目工程化过程中,Wire 可以很好协助我们完成复杂对象的构建组装。

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

7. 关于我们

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。

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

图片


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK