您现在的位置是:网站首页> 编程资料编程资料

Go语言官方依赖注入工具Wire的使用教程_Golang_

2023-05-26 410人已围观

简介 Go语言官方依赖注入工具Wire的使用教程_Golang_

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 } 

在这段代码里,层级依赖关系为 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 ®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/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:genera
                
                

-六神源码网