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推荐》,主要是每周推荐一个库或者好的项目,然后写一点这个库使用方法或者优点之类的,这样可以真正的帮助到大家能够学习到新的库,并且知道怎么用。

大概规则和每日新闻类似,如果报名人多的话每个人一个月轮到一次,欢迎大家报名!

点击 阅读原文 即刻报名