go依赖注入--google开源库wire

CKeengolanggolang的常用库约 2504 字大约 8 分钟

作者:程序员CKeen
博客:http://ckeen.cnopen in new window

长期坚持做有价值的事!积累沉淀,持续成长,升维思考!希望把编码作为长期兴趣爱好😄


如果使用过java的小伙伴一定对**依赖注入( dependency injection)**很熟悉了,依赖项注入是一种标准的技术,通过显式地向组件提供它们工作所需的所有依赖项,来生成灵活且松散耦合的代码。现在有很多依赖注入框架,go语言方面呢,就有Uber的dig和Facebook的inject都使用反射来进行运行时依赖注入。Wire主要受到Java Dagger 2的启发,使用代码生成而不是反射或服务定位器

今天我们主要一下go中的依赖注入工具wire。wire是google的开源库go-cloudopen in new window使用的依赖注入工具

1.wire的使用好处

在Go中,这通常采取的形式是将依赖传递给构造函数:

func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error)

这种传递依赖给构造函数的方式,在应用程序较小的时候可以很好的工作,如果在一个大的应用中,依赖关系图可能很复杂,大量的初始化代码依赖于构造函数的依赖顺序时,这时处理起来就没那么容易了。

而且当某一个依赖项,要被多个构造函数依赖,每个都要手动创建对象,传入构造,看起来代码也没那么整洁。另外一个问题是,如果在复杂的依赖关系图中,某个服务要被另一个服务替换掉的时候会很头疼。因为我们要在复杂的依赖关系图中,找到所有依赖旧的服务的类,然后用构造的新的服务去替换它,这种去修改初始化代码的是繁琐而缓慢的。

而使用wire依赖注入的工具来管理初始化代码的优势在这里就提现出来了。我们可以将服务及其依赖项描述为代码或者配置,然后wire就会自动构造出依赖关系图,然后为每个服务传递构造服务所需要的依赖项。当需求修改依赖签名或者删除添加服务的依赖项时,只要修改相应的代码和配置,wire将自动为我们完成这些服务依赖项的注入工作。

wire官方给的几个优点:

  • 当依赖图变得复杂时,运行时依赖注入可能很难跟踪和调试。使用代码生成意味着在运行时执行的初始化代码是常规的、习惯的Go代码,易于理解和调试。没有什么东西会被一个做“魔术”的介入框架所混淆。特别是,像忘记依赖关系这样的问题变成了编译时错误,而不是运行时错误。
  • 与服务定位器不同,注册服务不需要编写任意的名称或键。Wire使用Go类型将组件与其依赖项连接起来。
  • 这更容易避免依赖膨胀。Wire生成的代码将只导入您需要的依赖项,因此您的二进制文件不会有未使用的导入。运行时依赖注入器直到运行时才能识别未使用的依赖。
  • Wire的依赖关系图是静态可知的,这为工具和可视化提供了机会。

2.安装wire工具

安装wire之前需要安装go的开发环境这里我们就不赘述了。这里直接安装wire工具

go install github.com/google/wire/cmd/wire@latest

可以去$GOPATH/bin目录看下,确保wire安装到bin的执行目录。

3.wire是怎么工作的

Wire有两个基本概念:提供者(providers)**和**注入器(injectors)

提供者(providers)是一个普通的go函数, 它提供给定依赖项的值,这些值可以简单的描述为函数的参数。下面我们定义三个具有依赖关系的程序示例代码:

// 生成UserStore对象,依赖于配置Config和DB
func NewUserStore(cfg *Config, db *gorm.DB) (*UserStore, error) {...}

// 提供的Config生成,没有依赖参数
func NewDefaultConfig() *Config {...}

// 提供生成DB的实例,同时依赖ConnectionInfo参数
func NewDB(info *ConnectionInfo) (*gorm.DB, error) {...}

这里初始化函数需要实例返回值提供了其他的函数依赖的的值,即为依赖的提供者。

注入器(injectors)是按依赖顺序调用提供程序的生成函数。您编写注入器的签名,包括任何需要的输入作为参数,并插入对wire的调用。使用构建最终结果所需的提供程序或提供程序集列表进行构建:

var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)

wire提供将需要依赖的函数签名,作为参数构建一个提供者程序集的列表。这样wire就能按照提供顺序的程序集,来实际构建出函数初始化过程代码。

最后通过wire.go的build函数提供对外的调用函数,生成实际代码:

func InitUserStore(info *ConnectionInfo) (*UserStore, error) {
		wire.Build(UserStoreSet, NewDB)
		return nil, nil
}

然后在wire.go的目录下执行wire命令:

wire

执行wire指令后,在wire.go的目录会生成一个wire_gen.go的文件,我们打开该文件可以看到wire给我们做了些啥

// Code generated by Wire. DO NOT EDIT.

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package user

// Injectors from wire.go:

func InitUserStore(info *ConnectionInfo) (*UserStore, error) {
	config := NewDefaultConfig()
	db, err := NewDB(info)
	if err != nil {
		return nil, err
	}
	userStore, err := NewUserStore(config, db)
	if err != nil {
		return nil, err
	}
	return userStore, nil
}

我可以看到wire根据我们在wire.go中的初始化函数签名,按实际构造顺序,生成了一个相同的实际初始化每个依赖项的初始化函数。对外直接调用该初始化构造函数即可。

这是一个只有三个组件的简单示例,因此手工编写初始化器不会太痛苦,但Wire为具有更复杂依赖关系图的组件和应用程序节省了大量手工工作。

4.我们怎么来使用wire

根据上面的wire的介绍wire的是怎么工作了,我们已经大致了解wire的使用方式,下面我们具体看下实现代码。

首先我们我们还是按上面的例子来构建相应的结构:

// user_service.go

package user

import (
	"github.com/google/wire"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 配置类
type Config struct {

}

// 返回用户UserStore对象,实际是查询数据返回的model
type UserStore struct {
	Name string
	Age int32
}

// 构造UserStore方法,实际应该是db查询的数据
func NewUserStore(cfg *Config, db *gorm.DB) (*UserStore, error) {
	// todo 从db从去查询数据
	return &UserStore{}, nil
}

// 默认的构造Config的方法
func NewDefaultConfig() *Config {
	return &Config{}
}

// 数据库连接信息结构
type ConnectionInfo struct {
	Driver string
	Source string
}

// DB的构造对象
func NewDB(info *ConnectionInfo) (*gorm.DB, error) {
	return gorm.Open(mysql.Open(info.Source))
}

// wire提供的构造Set集
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)

下面我们实现wire.go的文件,用来生成相应的调用的函数

// +build wireinject

package user

import "github.com/google/wire"

func InitUserStore(info *ConnectionInfo) (*UserStore, error) {
	wire.Build(UserStoreSet, NewDB)
	return nil, nil
}

wire的构造文件,“ // +build wireinject” 这个必须加上,不然后面生成wire_gen.go的方法会冲突。

实际执行wire执行后,会生成wire.gen的文件

package user

// Injectors from wire.go:

func InitUserStore(info *ConnectionInfo) (*UserStore, error) {
	config := NewDefaultConfig()
	db, err := NewDB(info)
	if err != nil {
		return nil, err
	}
	userStore, err := NewUserStore(config, db)
	if err != nil {
		return nil, err
	}
	return userStore, nil
}

然后直接调用即可:

// main.go

func main() {
		info := &user.ConnectionInfo{
			Driver: "mysql",
			Source: "root:root@tcp(127.0.0.1:3333)/user?charset=utf8mb4&parseTime=True&loc=Local",
		}
		userStore, _ := user.InitUserStore(info)
		log.Printf("user store result:%v\n", userStore)
}

5. wire的高级特性

  • ProviderSet的嵌套调用

在上述的例子中,使用wire.NewSet构造的Set集如下:

// wire提供的构造Set集
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)

这里除了使用构造函数前面,我们还可以传入其他默认的wire.NewSet构造的Set集合,相当于我们可以嵌套构造。例如,如果NewDefaultConfig的也是一个wire.NewSet构造的集合,如下:

// config.go
var ProviderConfigSet = wire.NetSet(NewConfig, NewOptions)

那么上面的使用wire构造UserStoreSet的则可以写成:

// wire提供的构造Set集
var UserStoreSet = wire.NewSet(NewUserStore, ProviderConfigSet)

这种嵌套的方式可以将多个类型有相同的依赖的多个构造器打包成一个集合,后续只需要使用这个集合即可。

同样的,在wire.Build中我们也可以直接传入相应的UserStoreSet

  • 使用wire.Struct将Provider注入struc中

    type Foo int
    type Bar int
    
    func ProvideFoo() Foo {
    	return 1
    }
    
    func ProvideBar() Bar {
    	return 2
    }
    
    type FooBar struct {
    	MyFoo Foo
    	MyBar Bar
    }
    
    var Set = wire.NewSet(
    	ProvideFoo,
    	ProvideBar,
    	wire.Struct(new(FooBar), "MyFoo", "MyBar"))
    
    

    可以使用wire.Sturct构造,将FooBar结构依赖的MyFoo和MyBar字段,通过ProvideFoo和ProvideBar提供的值注入到FooBar结构,通过如下wire的构造文件,我们可以看下具体实现:

    // wire.go
    func CreateFoobar() (*FooBar, error) {
    	wire.Build(Set)
    	return nil, nil
    }
    

    生成的wire_gen.go文件,可以看到具体的实现:

    // wire_gen.go
    func CreateFoobar() (*FooBar, error) {
    	foo := ProvideFoo()
    	bar := ProvideBar()
    	fooBar := &FooBar{
    		MyFoo: foo,
    		MyBar: bar,
    	}
    	return fooBar, nil
    }
    
  • 使用wire的Value绑定注入值

    使用wire.Value绑定一个实际依赖的值,下面代码中,我们可以看到通过Value来构造一个Foo struct的值。

    type Foo struct {
        X int
    }
    
    func injectFoo() Foo {
        wire.Build(wire.Value(Foo{X: 42}))
        return Foo{}
    }
    

    使用InterfaceValue绑定实现接口的Value

    func CreateReader() io.Reader {
        wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
        return nil
    }
    

    将io.Reader的接口实现绑定到os.Stdin输入实现上,用来构造io.Reader的依赖项。

    注: wire具体实现可以参照上面去查看具体的实现代码

6.参考资料

wire博客:https://go.dev/blog/wireopen in new window

官方guide: https://github.com/google/wire/blob/main/docs/guide.mdopen in new window