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

本文中涉及到的相关代码,都已上传至:github.com/chenmingyon...

前言

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

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

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

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

Wire

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

Wire 安装

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

bash 复制代码
go install github.com/google/wire/cmd/wire@latest

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

Wire 的基本使用

前置代码准备

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

kotlin 复制代码
go get github.com/google/wire@latest

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

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

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

    go 复制代码
    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 接口类型:

    go 复制代码
    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 结构体的方法创建路由:

    go 复制代码
    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 复制代码
    //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 文件,内容如下所示:

    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 函数:

go 复制代码
func NewPostHandler(serv service.IPostService) *PostHandler {
    return &PostHandler{serv: serv}
}

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

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

go 复制代码
package handler

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

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

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

go 复制代码
//go:build wireinject

package wire

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

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

Wire 注入器(injectors)

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

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

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

Wire 的高级用法

绑定接口

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

go 复制代码
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 的值:

go 复制代码
func NewPostServiceV2() *PostService {
    return &PostService{}
}

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

go 复制代码
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,它能根据现有的类型进行构造结构体,我们来看看下面的例子:

go 复制代码
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 工具生成的代码如下所示:

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

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

绑定值

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

go 复制代码
func InjectUser() User {
    wire.Build(wire.Value(User{MyName: "陈明勇"}))
    return User{}
}

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

go 复制代码
func InjectUser() User {
    user := _wireUserValue
    return user
}

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

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

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

go 复制代码
func InjectPostService() service.IPostService {
    wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{}))
    return nil
}

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

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

go 复制代码
func GetUserName() Name {
    wire.Build(
       NewUser,
       wire.FieldsOf(new(User), "MyName"),
    )
    return ""
}

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

go 复制代码
func GetUserName() Name {
    user := NewUser()
    name := user.MyName
    return name
}

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

清理函数

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

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 来更简洁地写它:

go 复制代码
func InitializeGin() *gin.Engine {
    panic(wire.Build(/* ... */))
}

小结

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

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

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

本文中涉及到的相关代码,都已上传至:github.com/chenmingyon...

参考文档

github.com/google/wire...

相关推荐
鬼火儿7 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin7 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧8 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧8 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧8 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧8 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧8 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧8 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧8 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang9 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构