深入浅出go依赖注入工具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 的高级用法
绑定接口
回顾我们之前编写的代码:
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技术干货")} }
清理函数
如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。
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
语言中变得更简单。
以上就是深入浅出go依赖注入工具Wire的使用的详细内容,更多关于go依赖注入工具Wire的资料请关注脚本之家其它相关文章!
相关文章
Golang Printf,Sprintf,Fprintf 格式化详解
这篇文章主要介绍了Golang Printf,Sprintf,Fprintf 格式化详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧2021-03-03
最新评论