在前一阵介绍单元测试的系列文章中,曾经简单介绍过wire依赖注入框架。但当时的wire还处于alpha阶段,不过最近wire已经发布了首个beta版,API发生了一些变化,同时也承诺除非万不得已,将不会破坏API的兼容性。在前文中,介绍了一些wire的基本概况,本篇就再也不重复,感兴趣的小伙伴们能够回看一下:搞定Go单元测试(四)—— 依赖注入框架(wire)。本篇将具体介绍wire的使用方法和一些最佳实践。mysql
本篇中的代码的完整示例能够在这里找到:wire-examplesgit
go get github.com/google/wire/cmd/wire
复制代码
咱们先经过一个简单的例子,让小伙伴们对wire
有一个直观的认识。下面的例子展现了一个简易wire
依赖注入示例:github
$ ls
main.go wire.go
复制代码
main.gosql
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
命令生成依赖文件:api
$ 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生成的文件bash
// 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 使用后restful
...
/*
// 使用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
了,对比使用前,不只减小了三行代码,而且无需再关心依赖之间的初始化顺序。框架
示例传送门: quickstartide
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),对象应当依赖于接口,而不是直接依赖于具体实现。
抽象成接口依赖更有助于单元测试哦!
搞定Go单元测试(一)——基础原理
搞定Go单元测试(二)—— mock框架(gomock)
在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
当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的返回值个数和顺序有所规定:
- 第一个参数是须要生成的依赖对象
- 若是返回2个返回值,第二个参数必须是func()或者error
- 若是返回3个返回值,第二个参数必须是func(),第三个参数则必须是error
示例传送门: cleanup-functions
当一些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
的角色,相似于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
因为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
若是一个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
中的变量类型不能重复,须要定义许多额外的基础类型别名。
目前wire
命令还不能识别_test.go
结尾文件中的provider
函数,这样就意味着若是须要在测试中也使用wire
来注入咱们的mock对象,咱们须要在常规代码中嵌入mock对象的provider
,这对常规代码有侵入性,不过官方彷佛也已经注意到了这个问题,感兴趣的小伙伴能够关注一下这条issue:github.com/google/wire…