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...

相关推荐
努力的布布24 分钟前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
PacosonSWJTU29 分钟前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc
记得开心一点嘛38 分钟前
在Java项目中如何使用Scala实现尾递归优化来解决爆栈问题
开发语言·后端·scala
黄俊懿1 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
2401_857439692 小时前
“衣依”服装销售平台:Spring Boot技术应用与优化
spring boot·后端·mfc
Jerry.ZZZ2 小时前
系统设计,如何设计一个秒杀功能
后端
27669582923 小时前
京东e卡滑块 分析
java·javascript·python·node.js·go·滑块·京东
企业码道刘某3 小时前
EasyExcel 大数据量导入导出解决方案进阶
java·后端·mybatis
九圣残炎4 小时前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端