目录
  • 1. 前言
  • 2. 依赖注入(DI)是什么
  • 3. Wire Come
    • 3.1 简介
    • 3.2 快速使用
    • 3.3 基础概念
  • 4. Wire使用实践
    • 4.1 基础使用
    • 4.2 高级特性
    • 4.3 高阶使用
  • 5. 注意事项
    • 5.1 相同类型问题
    • 5.2 单例问题
  • 6. 结语

    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
    }
    

    在这段代码里,层级依赖关系为 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()
    }
    

    2.编写一个 wire.go 文件,用于 wire 工具来解析依赖,生成代码:

    //+build wireinject
    
    package demo
    
    func InitializeBroadCast() BroadCast {
        wire.Build(NewBroadCast, NewChannel, NewMessage)
        return BroadCast{}
    }
    

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

    3.使用 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 这两行之间是否有空行,这个空行必须要有!见https://github.com/google/wire/issues/117

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

    • 第一个返回值是需要生成的对象
    • 如果有 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()
    

    更多用法具体可以参考 wire官方指南:https://github.com/google/wire/blob/main/docs/guide.md

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

    6. 结语

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

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

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。