酷Go推荐 依赖注入工具代码生成器 wire

jacobxy · 2021年03月19日 · 806 次阅读
本帖已被设为精华帖!

Golang | wire 库

推荐编辑:不落凡尘

简介

wire 是一个代码生成工具,它通过自动生成代码的方式完成依赖注入。

应用场景

wire 作为依赖注入的代码生成工具,非常适合复杂对象的创建。而在大型项目中,拥有一个合适的依赖注入的框架将使得项目的开发与维护十分便捷。

Wire 核心概念

wire 中最核心的两个概念就是 Injector 和 Provider。

Provider : 生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回

Injector : 代表了我们最终要生成的构建函数的函数签名,返回值代表了构建的目标,在最后生成的代码中,此函数签名会完整的保留下来。

安装

go get github.com/google/wire/cmd/wire

代码生成

命令行在指定目录下执行 wire命令即可。

示例学习

官方示例

成员介绍

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))

示例

这样将 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")

示例

这里的代码是等价的,但是却不能和上面的代码共存,原因稍后会解释。

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 
}

示例

可以看到当 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()
    }   
}

示例

就这样名为 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 中

示例

个人观点 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
...

示例

小结

wire 是一个强大的工具,它在不运行 Go 程序的基础上,借助于特定文件 ("//+build wireinject") 的解析,自动生成对象的构造函数代码。

Go 语言工程化的过程中,涉及到诸多对象的包级别归类,wire 可以很好的协助我们完成复杂对象的构建过程。

还想了解更多吗?

更多请查看: https://github.com/google/wire

欢迎加入我们 GOLANG 中国社区:https://gocn.vip

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 03月19日 10:10
moss GoCN 每日新闻 (2021-03-20) 中提及了此贴 03月20日 06:19
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册