Golang依赖注入框架wire使用详解

What is wire?

wire是google开源的依赖注入框架。或者引用官方的话来说:“Wire is a code generation tool that automates connecting components using dependency injection.”。

官方地址:https://github.com/google/wire

Why wire?

除了wire,Go的依赖注入框架还有Uber的dig和Facebook的inject,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection),而wire则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection)。使用反射带来的性能损失倒是其次,更重要的是反射使得代码难以追踪和调试(反射会令Ctrl+左键失效…)。而wire生成的代码是符合程序员常规使用习惯的代码,十分容易理解和调试。
关于wire的优点,在官方博文上有更详细的的介绍:blog.golang.org/wire

Provider & Injector

providerinjectorwire的两个核心概念。

provider: a function that can produce a value. These functions are ordinary Go code.
injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body.

通过提供provider函数,让wire知道如何产生这些依赖对象。wire根据我们定义的injector函数签名,生成完整的injector函数,injector函数是最终我们需要的函数,它将按依赖顺序调用provider

provider

provider就是普通的Go函数,可以把它看作是某对象的构造函数,我们通过provider告诉wire该对象的依赖情况:

// NewUserStore是*UserStore的provider,表明*UserStore依赖于*Config和 *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

// NewDefaultConfig是*Config的provider,没有依赖
func NewDefaultConfig() *Config {...}

// NewDB是*mysql.DB的provider,依赖于ConnectionInfo
func NewDB(info ConnectionInfo) (*mysql.DB, error) {...}

// UserStoreSet 可选项,可以使用wire.NewSet将通常会一起使用的依赖组合起来。
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)
injector

injectorwire生成的函数,我们通过调用injector来获取我们所需的对象或值,injector会按照依赖关系,按顺序调用provider函数:

// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject

// initUserStore是由wire生成的injector
func initUserStore(info ConnectionInfo) (*UserStore, error) {
    // *Config的provider函数
    defaultConfig := NewDefaultConfig()
    // *mysql.DB的provider函数
    db, err := NewDB(info)
    if err != nil {
        return nil, err
    }
    // *UserStore的provider函数
    userStore, err := NewUserStore(defaultConfig, db)
    if err != nil {
        return nil, err
    }
    return userStore, nil
}

injector帮我们把按顺序初始化依赖的步骤给做了,我们在main.go中只需要调用initUserStore方法就能得到我们想要的对象了。

那么wire是怎么知道如何生成injector的呢?我们需要写一个函数来告诉它:

  • 定义injector的函数签名
  • 在函数中使用wire.Build方法列举生成injector所需的provider
例如:
// initUserStore用于声明injector的函数签名
func initUserStore(info ConnectionInfo) (*UserStore, error) {  
    // wire.Build声明要获取一个UserStore需要调用到哪些provider函数
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // 这些返回值wire并不关心。
}

有了上面的函数,wire就可以得知如何生成injector了。wire生成injector的步骤描述如下:

  1. 确定所生成injector函数的函数签名:func initUserStore(info ConnectionInfo) (*UserStore, error)
  2. 感知返回值第一个参数是*UserStore
  3. 检查wire.Build列表,找到*UserStoreprovider:NewUserStore
  4. 由函数签名func NewUserStore(cfg *Config, db *mysql.DB)得知NewUserStore依赖于*Config, 和*mysql.DB
  5. 检查wire.Build列表,找到*Config*mysql.DBprovider:NewDefaultConfigNewDB
  6. 由函数签名func NewDefaultConfig() *Config得知*Config没有其他依赖了。
  7. 由函数签名func NewDB(info *ConnectionInfo) (*mysql.DB, error)得知*mysql.DB依赖于ConnectionInfo
  8. 检查wire.Build列表,找不到ConnectionInfoprovider,但在injector函数签名中发现匹配的入参类型,直接使用该参数作为NewDB的入参。
  9. 感知返回值第二个参数是error
  10. 按依赖关系,按顺序调用provider函数,拼装injector函数。

最佳实践

Installing
$ go get github.com/google/wire/cmd/wire
Quick Start

我们先通过一个简单的例子,让小伙伴们对wire有一个直观的认识。下面的例子展示了一个简易wire依赖注入示例:

$ ls
main.go  wire.go 
main.go
package main

import "fmt"

type Message struct {
	msg string
}
type Greeter struct {
	Message Message
}
type Event struct {
	Greeter Greeter
}
// NewMessage Message的构造函数
func NewMessage(msg string) Message {
	return Message{
		msg:msg,
	}
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
	return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
	return Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
func (g Greeter) Greet() Message {
	return g.Message
}

// 使用wire前
func main() {
	message := NewMessage("hello world")
	greeter := NewGreeter(message)
	event := NewEvent(greeter)

	event.Start()
}
/*
// 使用wire后
func main() {
	event := InitializeEvent("hello_world")

	event.Start()
}*/
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"

// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
	wire.Build(NewEvent, NewGreeter, NewMessage)
	return Event{}  //返回值没有实际意义,只需符合函数签名即可
}

调用wire命令生成依赖文件:

$ wire
wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
$ ls
main.go  wire.go  wire_gen.go
wire生成的文件: wire_gen.go
// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent(msg string) Event {
	message := NewMessage(msg)
	greeter := NewGreeter(message)
	event := NewEvent(greeter)
	return event
}

使用前 V.S 使用后

/*
// 使用wire前
func main() {
	message := NewMessage("hello world")
	greeter := NewGreeter(message)
	event := NewEvent(greeter)

	event.Start()
}*/

// 使用wire后
func main() {
	event := InitializeEvent("hello_world")

	event.Start()
}

示例代码: quickstart

quickstart的例子中,NewMessage,NewGreeter,NewEvent都是providerwire_gen.go中的InitializeEvent函数是injector,可以看到injector通过按依赖顺序调用provider来生成我们需要的对象Event。

上述示例在wire.go中定义了injector的函数签名,注意要在文件第一行加上

// +build wireinject
...

用于告诉编译器无需编译该文件。在injector的签名定义函数中,通过调用wire.Build方法,指定用于生成依赖的provider:

// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
	wire.Build(NewEvent, NewGreeter, NewMessage) // <--- 传入provider函数
	return Event{}  //返回值没有实际意义,只需符合函数签名即可
}

该方法的返回值没有实际意义,只需要符合函数签名的要求即可。

高级特性

接口绑定

根据依赖倒置原则(Dependence Inversion Principle),对象应当依赖于接口,而不是直接依赖于具体实现。

quickstart的例子中的依赖均是具体实现,现在我们来看看在wire中如何处理接口依赖:

// UserService 
type UserService struct {
	userRepo UserRepository // <-- UserService依赖UserRepository接口
}

// UserRepository 存放User对象的数据仓库接口,比如可以是mysql,restful api ....
type UserRepository interface {
	// GetUserByID 根据ID获取User, 如果找不到User返回对应错误信息
	GetUserByID(id int) (*User, error)
}
// NewUserService *UserService构造函数
func NewUserService(userRepo UserRepository) *UserService {
	return &UserService{
		userRepo:userRepo,
	}
}

// mockUserRepo 模拟一个UserRepository实现
type mockUserRepo struct {
	foo string
	bar int
}
// GetUserByID UserRepository接口实现
func (u *mockUserRepo) GetUserByID(id int) (*User,error){
	return &User{}, nil
}
// NewMockUserRepo *mockUserRepo构造函数
func NewMockUserRepo(foo string,bar int) *mockUserRepo {
	return &mockUserRepo{
		foo:foo,
		bar:bar,
	}
}
// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

在这个例子中,UserService依赖UserRepository接口,其中mockUserRepoUserRepository的一个实现,由于在Go的最佳实践中,更推荐返回具体实现而不是接口。所以mockUserRepoprovider函数返回的是*mockUserRepo这一具体类型。wire无法自动将具体实现与接口进行关联,我们需要显示声明它们之间的关联关系。通过wire.NewSetwire.Bind*mockUserRepoUserRepository进行绑定:

/ MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

定义injector函数签名:

func InitializeUserService(foo string, bar int) *UserService{
	wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
	return nil
}

示例代码: binding-interfaces

组合Provider

当一些provider通常是一起使用的时候,可以使用provider set将它们组织起来,以quickstart示例为模板稍作修改:

// NewMessage Message的构造函数
func NewMessage(msg string) Message {
	return Message{
		msg:msg,
	}
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
	return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
	return Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
// EventSet Event通常是一起使用的一个集合,使用wire.NewSet进行组合
var EventSet  = wire.NewSet(NewEvent, NewMessage, NewGreeter)

述例子中将Event和它的依赖通过wire.NewSet组合起来,作为一个整体在injector函数签名定义中使用:

func InitializeEvent(msg string) Event{
	//wire.Build(NewEvent, NewGreeter, NewMessage)
	wire.Build(EventSet) 
	return Event{}
}

这时只需将EventSet传入wire.Build即可。

示例代码: provider-set

结构体provider

除了函数外,结构体也可以充当provider的角色,类似于setter注入:

ype 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.Struct来指定那些字段要被注入到结构体中,如果是全部字段,也可以简写成:

var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "*")) // * 表示注入全部字段

生成的injector函数:

func InitializeFooBar() FooBar {
	foo := ProvideFoo()
	bar := ProvideBar()
	fooBar := FooBar{
		MyFoo: foo,
		MyBar: bar,
	}
	return fooBar
}

示例代码: struct-provider

区分类型

由于injector的函数中,不允许出现重复的参数类型,否则wire将无法区分这些相同的参数类型,比如:

type FooBar struct {
	foo string
	bar string
}

func NewFooBar(foo string, bar string) FooBar {
	return FooBar{
	    foo: foo,  
	    bar: bar,
	}
}

injector函数签名定义:

// wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系
func InitializeFooBar(a string, b string) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}

如果使用上面的provider来生成injector,wire会报如下错误:

provider has multiple parameters of type string

因为入参均是字符串类型,wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系。 所以我们使用不同的类型来避免冲突:

type Foo string
type Bar string
type FooBar struct {
	foo Foo
	bar Bar
}

func NewFooBar(foo Foo, bar Bar) FooBar {
	return FooBar{
	    foo: foo,
	    bar: bar,
	}
}

injector函数签名定义:

func InitializeFooBar(a Foo, b Bar) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}

其中基础类型和通用接口类型是最容易发生冲突的类型,如果它们在provider函数中出现,最好统一新建一个别名来代替它(尽管还未发生冲突),例如:

type MySQLConnectionString string
type FileReader io.Reader

示例代码: distinguishing-types

Options 结构体

如果一个provider方法包含了许多依赖,可以将这些依赖放在一个options结构体中,从而避免构造函数的参数太多:

type Message string

// Options
type Options struct {
	Messages []Message
	Writer   io.Writer
	Reader   io.Reader
}
type Greeter struct {
}

// NewGreeter Greeter的provider方法使用Options以避免构造函数过长
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
	return nil, nil
}
// GreeterSet 使用wire.Struct设置Options为provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)

injector函数签名:

func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
	wire.Build(GreeterSet)
	return nil, nil
}

示例代码: options-structs

error返回

在前面的例子中,我们的provider函数均只有一个返回值,但在某些情况下,provider函数可能会对入参做校验,如果参数错误,则需要返回errorwire也考虑了这种情况,provider函数可以将返回值的第二个参数设置成error:

// Config 配置
type Config struct {
    // RemoteAddr 连接的远程地址
	RemoteAddr string
	
}
// APIClient API客户端
type APIClient struct {
	c Config
}
// NewAPIClient  APIClient构造函数,如果入参校验失败,返回错误原因
func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二个参数设置成error
	if c.RemoteAddr == "" {
		return nil, errors.New("没有设置远程地址")
	}
	return &APIClient{
		c:c,
	},nil
}
// Service
type Service struct {
	client *APIClient
}
// NewService Service构造函数
func NewService(client *APIClient) *Service{
	return &Service{
		client:client,
	}
}

类似的,injector函数定义的时候也需要将第二个返回值设置成error

...
func InitializeClient(config Config) (*Service, error) { // <-- 第二个参数设置成error
	wire.Build(NewService,NewAPIClient)
	return nil,nil
}
...

观察一下wire生成的injector

func InitializeClient(config Config) (*Service, error) {
	apiClient, err := NewAPIClient(config)
	if err != nil { // <-- 在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误
		return nil, err
	}
	service := NewService(apiClient)
	return service, nil
}

在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误。

示例代码: return-error

Cleanup 处理

provider生成的对象需要一些cleanup处理,比如关闭文件,关闭数据库连接等操作时,依然可以通过设置provider的返回值来达到这样的效果:

// FileReader
type FileReader struct {
	f *os.File
}
// NewFileReader *FileReader 构造函数,第二个参数是cleanup function
func NewFileReader(filePath string) (*FileReader, func(), error){
	f, err := os.Open(filePath)
	if err != nil {
	    return nil,nil,err
	}
	fr := &FileReader{
	    f:f,
	}
	fn := func() {
	    log.Println("cleanup") 
	    fr.f.Close()
	}
	return fr,fn,nil
}

跟返回错误类似,将provider的第二个返回参数设置成func()用于返回cleanup function,上述例子中在第三个参数中返回了error,但这是可选的:

wireprovider的返回值个数和顺序有所规定:

  • 第一个参数是需要生成的依赖对象
  • 如果返回2个返回值,第二个参数必须是func()或者error
  • 如果返回3个返回值,第二个参数必须是func(),第三个参数则必须是error

示例代码: cleanup-functions

参考文章: https://studygolang.com/articles/22266?fr=sidebar

本篇中的代码的完整示例可以在这里找到:wire-examples

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值