Go 项目必备:深入浅出 Wire 依赖注入工具

简介: 在本文中,我们详细探讨了 Go Wire 工具的基本用法和高级特性。它是一个专为依赖注入设计的代码生成工具,它不仅提供了基础的依赖解析和代码生成功能,还支持多种高级用法,如接口绑定和构造结构体。

本文中涉及到的相关代码,都已上传至:https://github.com/chenmingyong0423/blog/tree/master/tutorial-code/wire

前言

在日常项目开发中,我们经常会使用到依赖注入的设计模式,目的是为了降低代码组件之间的耦合度,提高代码的可维护性、可扩展性和可测试性。

但随着项目规模的增长,组件之间的依赖关系变得复杂,手动管理它们之间的依赖关系可能会很繁琐。为了简化这个过程,我们可以利用依赖注入代码生成工具,它可以自动为我们生成所需的代码,从而减轻了手动处理依赖注入的繁重工作。

Go 语言有许多依赖注入的工具,而本文将深入探讨一个备受欢迎的 Go 语言依赖注入工具—— Wire

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

Wire

Wire 是一个专为依赖注入(Dependency Injection)设计的代码生成工具,它可以自动生成用于初始化各种依赖关系的代码,从而帮助我们更轻松地管理和注入依赖关系。

Wire 安装

我们可以执行以下命令来安装 Wire 工具:

go install github.com/google/wire/cmd/wire@latest

安装之前请确保已将 $GOPATH/bin 添加到环境变量 $PATH 里。

Wire 的基本使用

前置代码准备

虽然我们在前面已经通过 go install 命令安装了 Wire 命令行工具,但在具体项目中,我们仍然需要通过以下命令安装项目所需的 Wire 依赖,以便结合 Wire 工具生成代码:

go get github.com/google/wire@latest

接下来,让我们模拟一个简单的 web 博客项目,编写查询文章接口的相关代码,并使用 Wire 工具生成代码。

首先,我们先定义相关类型与方法,并提供对应的 初始化函数

  • 定义 PostHandler 结构体,创建注册路由的方法 RegisterRoutes 和查询文章路由处理的方法 GetPostById 以及初始化的函数 NewPostHandler,并且它依赖于 IPostService 接口:

      package handler
    
      import (
          "chenmingyong0423/blog/tutorial-code/wire/internal/post/service"
          "github.com/gin-gonic/gin"
          "net/http"
      )
    
      type PostHandler struct {
         
          serv service.IPostService
      }
    
      func (h *PostHandler) RegisterRoutes(engine *gin.Engine) {
         
          engine.GET("/post/:id", h.GetPostById)
      }
    
      func (h *PostHandler) GetPostById(ctx *gin.Context) {
         
          content := h.serv.GetPostById(ctx, ctx.Param("id"))
          ctx.String(http.StatusOK, content)
      }
    
      func NewPostHandler(serv service.IPostService) *PostHandler {
         
          return &PostHandler{
         serv: serv}
      }
    
  • 定义 IPostService 接口,并提供了一个具体实现 PostService,接着创建 GetPostById 方法,用于处理查询文章的逻辑,然后提供初始化函数 NewPostService,该函数返回 IPostService 接口类型:

      package service
    
      import (
          "context"
          "fmt"
      )
    
      type IPostService interface {
         
          GetPostById(ctx context.Context, id string) string
      }
    
      var _ IPostService = (*PostService)(nil)
    
      type PostService struct {
         
      }
    
      func (s *PostService) GetPostById(ctx context.Context, id string) string {
         
          return fmt.Sprint("欢迎关注本掘金号,作者:陈明勇")
      }
    
      func NewPostService() IPostService {
         
          return &PostService{
         }
      }
    
  • 定义一个初始化 gin.Engine 函数 NewGinEngineAndRegisterRoute,该函数依赖于 *handler.PostHandler 类型,函数内部调用相关 handler 结构体的方法创建路由:

      package ioc
    
      import (
          "chenmingyong0423/blog/tutorial-code/wire/internal/post/handler"
          "github.com/gin-gonic/gin"
      )
    
      func NewGinEngineAndRegisterRoute(postHandler *handler.PostHandler) *gin.Engine {
         
          engine := gin.Default()
          postHandler.RegisterRoutes(engine)
          return engine
      }
    

    使用 Wire 工具生成代码

    前置代码已经准备好了,接下来我们编写核心代码,以便 Wire 工具能生成相应的依赖注入代码。

  • 首先我们需要创建一个 wire 的配置文件,通常命名为 wire.go。在这个文件里,我们需要定义一个或者多个注入器函数(Injector 函数,接下来的内容会对其进行解释),以便指引 Wire 工具生成代码。

      //go:build wireinject
    
      package wire
    
      import (
          "chenmingyong0423/blog/tutorial-code/wire/internal/post/handler"
          "chenmingyong0423/blog/tutorial-code/wire/internal/post/service"
          "chenmingyong0423/blog/tutorial-code/wire/ioc"
          "github.com/gin-gonic/gin"
          "github.com/google/wire"
      )
    
      func InitializeApp() *gin.Engine {
          wire.Build(
             handler.NewPostHandler,
             service.NewPostService,
             ioc.NewGinEngineAndRegisterRoute,
          )
          return &gin.Engine{}
      }
    

    在上述代码中,我们定义了一个用于初始化 gin.Engine 的注入器函数,在该函数内部,我们使用了 wire.Build 方法来声明依赖关系,其中包括 PostHandlerPostServiceInitGinEngine 作为依赖的构造函数。

    wire.Build 的作用是 连接或绑定我们之前定义的所有初始化函数。当我们运行 wire 工具来生成代码时,它就会根据这些依赖关系来自动创建和注入所需的实例。

    注意:文件首行必须加上 //go:build wireinject// +build wireinject(go 1.18 之前的版本使用) 注释,作用是只有在使用 wire 工具时才会编译这部分代码,其他情况下忽略。

  • 接下来在 wire.go 文件所处目录下执行 wire 命令,生成 wire_gen.go 文件,内容如下所示:

      // Code generated by Wire. DO NOT EDIT.
    
      //go:generate go run github.com/google/wire/cmd/wire
      //go:build !wireinject
      // +build !wireinject
    
      package wire
    
      import (
          "chenmingyong0423/blog/tutorial-code/wire/internal/post/handler"
          "chenmingyong0423/blog/tutorial-code/wire/internal/post/service"
          "chenmingyong0423/blog/tutorial-code/wire/ioc"
          "github.com/gin-gonic/gin"
      )
    
      // Injectors from wire.go:
    
      func InitializeApp() *gin.Engine {
         
          iPostService := service.NewPostService()
          postHandler := handler.NewPostHandler(iPostService)
          engine := ioc.NewGinEngineAndRegisterRoute(postHandler)
          return engine
      }
    

    生成的代码和我们手写区别不大,当我们的组件很多,依赖关系复杂的时候,我们才会感觉到 Wire 工具的好处。

Wire 的核心概念

Wire 有两个核心概念:提供者(providers)和注入器(injectors)。

Wire 提供者(providers)

提供者:一个可以产生值的函数,也就是有返回值的函数。例如入门代码里的 NewPostHandler 函数:

func NewPostHandler(serv service.IPostService) *PostHandler {
   
    return &PostHandler{
   serv: serv}
}

返回值不仅限于一个,如果有需要的话,可以额外添加一个 error 的返回值。

如果提供者过多的时候,我们还可以以分组的形式进行连接,例如将 post 相关的 handlerservice 进行组合:

package handler

var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)

使用 wire.NewSet 函数将提供者进行分组,该函数返回一个 ProviderSet 结构体。不仅如此,wire.NewSet 还能对多个 ProviderSet 进行分组 wire.NewSet(PostSet, XxxSet)

对于之前的 InitializeApp 函数,我们可以这样升级:

//go:build wireinject

package wire

func InitializeAppV2() *gin.Engine {
   
    wire.Build(
       handler.PostSet,
       ioc.NewGinEngineAndRegisterRoute,
    )
    return &gin.Engine{
   }
}

然后通过 Wire 命令生成代码,和之前的结果一致。

Wire 注入器(injectors)

注入器(injectors)的作用是将所有的提供者(providers)连接起来,回顾一下我们之前的代码:

func InitializeApp() *gin.Engine {
   
    wire.Build(
       handler.NewPostHandler,
       service.NewPostService,
       ioc.NewGinEngineAndRegisterRoute,
    )
    return &gin.Engine{
   }
}

InitializeApp 函数就是一个注入器,函数内部通过 wire.Build 函数连接所有的提供者,然后返回 &gin.Engine{},该返回值实际上并没有使用到,只是为了满足编译器的要求,避免报错而已,真正的返回值来自 ioc.NewGinEngineAndRegisterRoute

Wire 的高级用法

绑定接口

回顾我们之前编写的代码:

package handler

···

func NewPostHandler(serv service.IPostService) *PostHandler {
   
    return &PostHandler{
   serv: serv}
}

···

pakacge service

···

func NewPostService() IPostService {
   
    return &PostService{
   }
}

···

NewPostHandler 函数依赖于 service.IPostService 接口,NewPostService 函数返回的是 IPostService 接口的值,这两个地方的类型匹配,因此 Wire 工具能够正确识别并生成代码。然而,这并不是推荐的最佳实践。因为在 Go 中的 最佳实践 是返回 具体的类型 的值,所以最好让 NewPostService 返回具体类型 PostService 的值:

func NewPostServiceV2() *PostService {
   
    return &PostService{
   }
}

但是这样,Wire 工具将认为 IPostService 接口类型与 PostService 类型不匹配,导致生成代码失败。因此我们需要修改注入器的代码:

func InitializeAppV3() *gin.Engine {
   
    wire.Build(
       handler.NewPostHandler,
       service.NewPostServiceV2,
       ioc.NewGinEngineAndRegisterRoute,
       wire.Bind(new(service.IPostService), new(*service.PostService)),
    )
    return &gin.Engine{
   }
}

使用 wire.Bind 来建立接口类型和具体的实现类型之间的绑定关系,这样 Wire 工具就可以根据这个绑定关系进行类型匹配并生成代码。

wire.Bind 函数的第一个参数是指向所需接口类型值的指针,第二个实参是指向实现该接口的类型值的指针。

结构体提供者(Struct Providers)

Wire 库有一个函数是 wire.Struct,它能根据现有的类型进行构造结构体,我们来看看下面的例子:

package main

type Name string

func NewName() Name {
   
    return "陈明勇"
}

type PublicAccount string

func NewPublicAccount() PublicAccount {
   
    return "公众号:Go技术干货"
}

type User struct {
   
    MyName          Name
    MyPublicAccount PublicAccount
}

func InitializeUser() *User {
   
    wire.Build(
       NewName,
       NewPublicAccount,
       wire.Struct(new(User), "MyName", "MyPublicAccount"),
    )
    return &User{
   }
}

上述代码中,首先定义了自定义类型 NamePublicAccount 以及结构体类型 User,并分别提供了 NamePublicAccount 的初始化函数(providers)。然后定义一个注入器(injectorsInitializeUser,用于构造连接提供者并构造 *User 实例。

使用 wire.Struct 函数需要传递两个参数,第一个参数是结构体类型的指针值,另一个参数是一个可变参数,表示需要注入的结构体字段的名称集。

根据上述代码,使用 Wire 工具生成的代码如下所示:

func InitializeUser() *User {
   
    name := NewName()
    publicAccount := NewPublicAccount()
    user := &User{
   
       MyName:          name,
       MyPublicAccount: publicAccount,
    }
    return user
}

如果我们不想返回指针类型,只需要修改 InitializeUser 函数的返回值为非指针即可。

绑定值

有时候,我们可以在注入器中通过 值表达式 给一个类型进行赋值,而不是依赖提供者(providers)。

func InjectUser() User {
   
    wire.Build(wire.Value(User{
   MyName: "陈明勇"}))
    return User{
   }
}

在上述代码中,使用 wire.Value 函数通过表达式直接指定 MyName 的值,生成的代码如下所示:

func InjectUser() User {
   
    user := _wireUserValue
    return user
}

var (
    _wireUserValue = User{
   MyName: "陈明勇"}
)

需要注意的是,值表达式将被复制到生成的代码文件中。

对于接口类型,可以使用 InterfaceValue

func InjectPostService() service.IPostService {
   
    wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{
   }))
    return nil
}

使用结构体字段作为提供者(providers)

有些时候,你可以使用结构体的某个字段作为提供者,从而生成一个类似 GetXXX 的函数。

func GetUserName() Name {
   
    wire.Build(
       NewUser,
       wire.FieldsOf(new(User), "MyName"),
    )
    return ""
}

你可以使用 wire.FieldsOf 函数添加任意字段,生成的代码如下所示:

func GetUserName() Name {
   
    user := NewUser()
    name := user.MyName
    return name
}

func NewUser() User {
   
    return User{
   MyName: Name("陈明勇"), MyPublicAccount: PublicAccount("公众号:Go技术干货")}
}

清理函数

如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。

func provideFile(log Logger, path Path) (*os.File, func(), error) {
   
    f, err := os.Open(string(path))
    if err != nil {
   
        return nil, nil, err
    }
    cleanup := func() {
   
        if err := f.Close(); err != nil {
   
            log.Log(err)
        }
    }
    return f, cleanup, nil
}

备用注入器语法

如果你不喜欢将类似这种写法 → return &gin.Engine{} 放在你的注入器函数声明的末尾,你可以用 panic 来更简洁地写它:

func InitializeGin() *gin.Engine {
   
    panic(wire.Build(/* ... */))
}

小结

在本文中,我们详细探讨了 Go Wire 工具的基本用法和高级特性。它是一个专为依赖注入设计的代码生成工具,它不仅提供了基础的依赖解析和代码生成功能,还支持多种高级用法,如接口绑定和构造结构体。

依赖注入的设计模式应用非常广泛,Wire 工具让依赖注入在 Go 语言中变得更简单。

你用过 Wire 工具吗?欢迎评论区留言讨论!

本文中涉及到的相关代码,都已上传至:https://github.com/chenmingyong0423/blog/tree/master/tutorial-code/wire

参考文档

https://github.com/google/wire/blob/main/docs/guide.md

作者:陈明勇

个人网站:https://chenmingyong.cn

文章持续更新,如果本文能让您有所收获,欢迎点赞收藏加关注本号。

微信阅读可搜《Go技术干货》。这篇文章已被收录于 GitHub https://github.com/chenmingyong0423/blog ,欢迎大家 Star 催更并持续关注。

目录
相关文章
|
3月前
|
JSON 运维 Go
Go 项目配置文件的定义和读取
Go 项目配置文件的定义和读取
|
17天前
|
JSON 监控 安全
go语言选择合适的工具和库
【10月更文挑战第17天】
11 2
|
29天前
|
Go
使用go语言将A助手加入项目中
使用go语言将A助手加入项目中
22 2
|
1月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
54 11
|
1月前
|
存储 Kubernetes Go
Go语言项目组织架构
Go语言项目组织架构
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
130 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
3月前
|
API
企业项目迁移go-zero实战(二)
企业项目迁移go-zero实战(二)
|
3月前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
3月前
|
算法 程序员 编译器
Go deadcode:查找没意义的死代码,对于维护项目挺有用!
Go deadcode:查找没意义的死代码,对于维护项目挺有用!
|
3月前
|
Kubernetes 数据可视化 Java