Golang依赖注入框架wire全攻略
在前一阵介绍单元测试的系列文章中,曾经简单介绍过 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
provider
和 injector
是 wire
的两个核心概念。
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
都是 provider
, wire_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
接口,其中 mockUserRepo
是 UserRepository
的一个实现,由于在Go的最佳实践中,更推荐返回具体实现而不是接口。所以 mockUserRepo
的 provider
函数返回的是 *mockUserRepo
这一具体类型。 wire
无法自动将具体实现与接口进行关联,我们需要显示声明它们之间的关联关系。通过 wire.NewSet
和 wire.Bind
将 *mockUserRepo
与 UserRepository
进行绑定:
// 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
函数可能会对入参做校验,如果参数错误,则需要返回 error
。 wire
也考虑了这种情况, 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
,但这是可选的:
- 第一个参数是需要生成的依赖对象
- 如果返回2个返回值,第二个参数必须是func()或者error
- 如果返回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…