本文中涉及到的相关代码,都已上传至: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
方法来声明依赖关系,其中包括 PostHandler
、PostService
和 InitGinEngine
作为依赖的构造函数。
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
相关的 handler
和 service
进行组合:
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 的高级用法
绑定接口
回顾我们之前编写的代码:
代码语言: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
的值:
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{}
}
上述代码中,首先定义了自定义类型 Name
和 PublicAccount
以及结构体类型 User
,并分别提供了 Name
和 PublicAccount
的初始化函数(providers
)。然后定义一个注入器(injectors
)InitializeUser
,用于构造连接提供者并构造 *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技术干货")}
}
清理函数
如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。
代码语言: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
我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表