likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 14

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

前言

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

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

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 工具生成代码。

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

使用 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 工具吗?欢迎评论区留言讨论!

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

参考文档

github.com/google/wire…