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 催更并持续关注。

目录
相关文章
|
2月前
|
网络协议 Linux Go
分享一个go开发的工具-SNMP Server
分享一个go开发的工具-SNMP Server
77 0
|
12天前
|
前端开发 JavaScript Go
|
1月前
|
存储 Go 索引
go语言并发实战——日志收集系统(一) 项目前言
go语言并发实战——日志收集系统(一) 项目前言
go语言并发实战——日志收集系统(一) 项目前言
|
12天前
|
Go
Go 项目自动重载解决方案 —— Air 使用入门
**Air**: 提升Go开发效率的利器!自动重载工具,监听文件变化,实时编译运行,无需频繁重启。安装:启用Go Module后,运行`GO111MODULE=on go install github.com/cosmtrek/air@latest`。启动项目:`air`,配置文件默认为`air.toml`。集成到项目,忽略`tmp/`目录。让代码更改即时生效,专注编码,告别手动重启。适用于开发环境,生产环境禁用。[更多详情](https://github.com/cosmtrek/air)
14 1
|
2天前
|
Oracle 关系型数据库 MySQL
|
2月前
|
编译器 Go 索引
浅谈go语言中的符文字符处理工具
【5月更文挑战第20天】本文简述了Go 1.20之后的rune符文处理工具和函数,`unsafe`包新增了SliceData、String和StringData函数,支持直接将slice转换为array,明确了数组和结构体比较顺序。
36 1
浅谈go语言中的符文字符处理工具
|
15天前
|
Go 持续交付
使用 Makefile 管理和部署 Go 项目
在软件开发中,`Makefile` 用于自动化任务,提升效率。在Go项目中,它简化构建和部署。`Makefile`集成了编译、打包、清理和部署等任务,减少错误,提高效率。通过定义规则和依赖,`make`工具执行任务。示例展示了如何创建`Makefile`进行Go应用的自动化部署,包括构建、传输、停启服务。通过`make deploy-dev`一键执行部署流程。`Makefile`不仅简化部署,还可扩展实现更多复杂自动化,提升开发流程的专业性和效率。
14 0
|
2月前
|
数据可视化 算法 Java
了解go语言运行时工具的作用
【5月更文挑战第16天】本文简介`runtime`库提供系统调用包装、执行跟踪、内存分配统计、运行时指标和剖析支持。`internal/syscall`封装系统调用,保证uintptr参数有效。`trace`用于执行跟踪,捕获各种事件,如goroutine活动、系统调用和GC事件。`ReadMemStats`提供内存分配器统计。`metrics`接口访问运行时定义的度量,包括CPU使用、GC和内存信息。`coverage`支持代码覆盖率分析,`cgo`处理C语言交互,`pprof`提供性能剖析工具集成。这些功能帮助优化和理解Go程序的运行行为。
42 6
|
2月前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
44 2
|
2月前
|
运维 关系型数据库 MySQL
Serverless 应用引擎产品使用之在阿里函数计算中,部署Go项目可以区分环境如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。