【GoCN酷Go推荐】依赖注入工具代码生成器 wire
共 8104字,需浏览 17分钟
·
2021-03-19 11:28
Golang | wire库
简介
wire是一个代码生成工具,它通过自动生成代码的方式完成依赖注入。
应用场景
wire作为依赖注入的代码生成工具,非常适合复杂对象的创建。而在大型项目中,拥有一个合适的依赖注入的框架将使得项目的开发与维护十分便捷。
Wire核心概念
wire 中最核心的两个概念就是Injector和Provider。
Provider : 生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回
Injector : 代表了我们最终要生成的构建函数的函数签名,返回值代表了构建的目标,在最后生成的代码中,此函数签名会完整的保留下来。
安装
go get github.com/google/wire/cmd/wire
代码生成
命令行在指定目录下执行 wire
命令即可。
示例学习
https://github.com/google/wire/tree/main/internal/wire/testdata/
成员介绍
func NewSet(...interface{}) ProviderSet
func Build(...interface{}) string
func Bind(iface, to interface{}) Binding
func Struct(structType interface{}, fieldNames ...string) StructProvider
func FieldsOf(structType interface{}, fieldNames ...string) StructFields
func Value(interface{}) ProvidedValue
func InterfaceValue(typ interface{}, x interface{}) ProvidedValue
基础代码
main.go
package main
type Leaf struct {
Name string
}
type Branch struct{
L Leaf
}
type Root struct {
B Branch
}
func NewLeaf(name string) Leaf {return Leaf{Name:name}}
func NewBranch(l Leaf) Branch {return Branch{L:l}}
func NewRoot(b Branch) Root {return Root{B:b}}
wire.go
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"github.com/google/wire"
)
func InitRoot(name string) Root {
wire.Build(NewLeaf,NewBranch,NewRoot)
return Root{}
}
wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
// Injectors from wire.go:
func InitRoot(name string) Root {
leaf := NewLeaf(name)
branch := NewBranch(leaf)
root := NewRoot(branch)
return root
}
这里我们可以看到代码的生成是根据wire.Build参数的输入与输出类型来决定的。
wire.Build的参数是Provider的不定长列表。
wire包成员的作用
wire的成员每一个都是为了Provider服务的,他们各自有适用的场景。
NewSet
NewSet的作用是为了防止Provider过多导致混乱,它把一组业务相关的Provider放在一起组织成ProviderSet。
wire.go可以写成
var NewBranchSet = wire.NewSet(NewLeaf,NewBranch)
func InitRoot(name string) Root {
wire.Build(NewBranchSet,NewRoot)
return Root{}
}
值得注意的事,NewSet可以写在原结构体所在的文件中,以方便切换和维护。
Bind
Bind函数的作用是为了让接口类型参与wire的构建过程。wire的构建依靠的是参数的类型来组织代码,所以接口类型天然是不支持的。Bind函数通过将接口类型和实现类型绑定,来达到依赖注入的目的。
type Fooer interface{
HelloWorld()
}
type Foo struct{}
func (f Foo)HelloWorld(){}
var bind = wire.Bind(new(Fooer),new(Foo))
示例地址:https://github.com/google/wire/tree/main/internal/wire/testdata/BindInjectorArgPointer
这样将bind传入NewSet或Build中就可以将Fooer接口和Foo类型绑定。
这里需要特别注意,如果是*Foo实现了Fooer接口,需要将最后的new(Foo)改成new(*Foo)
Struct
Struct函数用于简化结构体的Provider,当结构体的Provider仅仅是字段赋值时可以使用这个函数。
//当Leaf中成员变量很多时,或者只需要部分初始化时,构造函数会变得很复杂
func NewLeaf(name string) Leaf {return Leaf{Name:name}}
//等价写法
//部分字段初始化
wire.Struct(new(Leaf),"Name")
//全字段初始化
wire.Struct(new(Leaf),"*")
这里的NewLeaf函数可以被下面的部分字段初始化函数替代。
Struct函数可以作为Provider出现在Build或NewSet的参数中。
FieldsOf
FieldsOf函数可以将结构体中的对应字段作为Provider,供wire使用。在上面的代码基础上,我们做如下的等价
//获得Leaf中Name字段的Provider
func NewName(l Leaf) string {return l.Name}
//等价写法
//FieldsOf的方式获得结构体内的字段
wire.FieldsOf(new(Leaf),"Name")
示例地址:https://github.com/google/wire/tree/main/internal/wire/testdata/FieldsOfStruct
这里的代码是等价的,但是却不能和上面的代码共存,原因稍后会解释。
Value
Value函数为基本类型的属性绑定具体值,在基于需求的基础上简化代码。
func NewLeaf()Leaf{
return Leaf{
Name:"leaf",
}
}
//等价写法
wire.Value(Leaf{Name:"leaf"})
以上两个函数在作为Provider上也是等价的,可以出现在Build或NewSet中。
InterfaceValue
InterfaceValue作用与Value函数类似,只是InterfaceValue函数是为接口类型绑定具体值。
wire.InterfaceValue(new(io.Reader),os.Stdin)
比较少用到,这里就不细讲了。
返回值的特殊情况
返回值 error
wire是支持返回对象的同时携带error的。对于error类型的返回值,wire也能很好的处理。
//main.go
func NewLeaf(name string) (Leaf, error) { return Leaf{Name: name}, nil }
//wire.go
func InitRoot(name string) (Root, error) {
...
}
//wire_gen.go
func InitRoot(name string) (Root, error) {
leaf, err := NewLeaf(name)
if err != nil {
return Root{}, err
}
branch := NewBranch(leaf)
root := NewRoot(branch)
return root, nil
}
示例地址:https://github.com/google/wire/tree/main/internal/wire/testdata/ReturnError
可以看到当Provider中出现error的返回值时,Injector函数的返回值中也必须携带error的返回值
清理函数CleanUp
清理通常出现在有文件对象,socket对象参与的构建函数中,无论是出错后的资源关闭,还是作为正常获得对象后的析构函数都是有必要的。
清理函数通常作为第二返回值,参数类型为func(),即为无参数无返回值的函数对象。跟error一样,当Provider中的任何一个拥有清理函数,Injector的函数签名返回值中也必须包含该函数类型。
//main.go
func NewLeaf(name string) (Leaf, func()) {
r := Leaf{Name: name}
return r, func() { r.Name = "" }
}
func NewBranch(l Leaf) (Branch, func()) { return Branch{L: l}, func() {} }
//wire.go
func InitRoot(name string) (Root, func()) {...}
//wire_gen.go
func InitRoot(name string) (Root, func()) {
leaf, cleanup := NewLeaf(name)
branch, cleanup2 := NewBranch(leaf)
root := NewRoot(branch)
return root, func() {
cleanup2()
cleanup()
}
}
示例地址:https://github.com/google/wire/tree/main/internal/wire/testdata/Cleanup
就这样名为cleanup的清理函数就随着InitRoot返回了。当有多个Provider有cleanup的时候,wire会自动把cleanup加入到最后的返回函数中。
常见问题
类型重复
基础类型
基础类型是构建结构体的基础,其作为参数创建结构体是十分常见的,参数类型重复更是不可避免的。wire通过Go语言语法中的"type A B"的方法来解决词类问题。
//wire.go
type Account string
func InitRoot(name string, account Account) (Root, func()) {...}
出现在wire.go中的"type A B" 会自动复制到wire_gen.go中
示例地址:https://github.com/google/wire/tree/main/internal/wire/testdata/InjectInput
个人观点 wire着眼于复杂对象的构建,因此基础类型的属性赋值推荐使用结构体本身的Set操作完成。
对象类型重复
每一个Provider都是一个组件的生成方法,如果有两个Provider生成同一类组件,那么在构建过程中就会产生冲突,这里需要特别注意,保证组件的类型唯一性。
循环构建
循环构建指的是多个Provider相互提供参数和返回值形成一个闭环。当wire检查构建的流程含有闭环构建的时候,就会报错。
type Root struct{
B Branch
}
type Branch struct {
L Leaf
}
type Leaf struct {
R Root
}
func NewLeaf(r Root) Leaf {return Leaf{R:r}}
func NewBranch(l Leaf) Branch {return Branch{L:l}}
func NewRoot(b Branch) Root {return Root{B:b}}
...
wire.Build(NewLeaf,NewRranch,NewRoot) //错误 cycle for XXX
...
示例地址:https://github.com/google/wire/tree/main/internal/wire/testdata/Cycle
小结
wire是一个强大的工具,它在不运行Go程序的基础上,借助于特定文件("//+build wireinject")的解析,自动生成对象的构造函数代码。
Go语言工程化的过程中,涉及到诸多对象的包级别归类,wire可以很好的协助我们完成复杂对象的构建过程。
还想了解更多吗?
更多请查看:https://github.com/tidwall/gjson
欢迎加入我们GOLANG中国社区:https://gocn.vip/
《酷Go推荐》招募:
各位Gopher同学,最近我们社区打算推出一个类似GoCN每日新闻的新栏目《酷Go推荐》,主要是每周推荐一个库或者好的项目,然后写一点这个库使用方法或者优点之类的,这样可以真正的帮助到大家能够学习到新的库,并且知道怎么用。
大概规则和每日新闻类似,如果报名人多的话每个人一个月轮到一次,欢迎大家报名!
点击 阅读原文 即刻报名