48

Golang依赖注入框架wire全攻略

 4 years ago
source link: https://www.tuicool.com/articles/YRvyaim
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在前一阵介绍单元测试的系列文章中,曾经简单介绍过 wire依赖注入框架 。但当时的wire还处于alpha阶段,不过最近wire已经发布了首个beta版,API发生了一些变化,同时也承诺除非万不得已,将不会破坏API的兼容性。在前文中,介绍了一些wire的基本概况,本篇就不再重复,感兴趣的小伙伴们可以回看一下: 搞定Go单元测试(四)—— 依赖注入框架(wire) 。本篇将具体介绍wire的使用方法和一些最佳实践。

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

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_gen.go wire生成的文件

// 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()
}
...
复制代码

使用 wire 后,只需调一个初始化方法既可得到 Event 了,对比使用前,不仅减少了三行代码,并且无需再关心依赖之间的初始化顺序。

示例传送门: quickstart

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.

github.com/google/wire…

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

在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{}  //返回值没有实际意义,只需符合函数签名即可
}
复制代码

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

高级特性

quickstart示例展示了 wire 的基础功能,本节将介绍一些高级特性。

接口绑定

根据依赖倒置原则(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 函数可能会对入参做校验,如果参数错误,则需要返回 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 functions

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 ,但这是可选的:

wire对provider的返回值个数和顺序有所规定:

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

示例传送门: cleanup-functions

Provider set

当一些 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 注入:

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

Best Practices

区分类型

由于 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 Structs

如果一个 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

一些缺点和限制

额外的类型定义

由于 wire 自身的限制, injector 中的变量类型不能重复,需要定义许多额外的基础类型别名。

mock支持暂时不够友好

目前 wire 命令还不能识别 _test.go 结尾文件中的 provider 函数,这样就意味着如果需要在测试中也使用 wire 来注入我们的mock对象,我们需要在常规代码中嵌入mock对象的 provider ,这对常规代码有侵入性,不过官方似乎也已经注意到了这个问题,感兴趣的小伙伴可以关注一下这条issue: github.com/google/wire…


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK