Golang依赖注入框架wire全攻略
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
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
,但这是可选的:
wire对provider的返回值个数和顺序有所规定:
- 第一个参数是需要生成的依赖对象
- 如果返回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…
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK