Golang依赖注入框架wire全攻略

https://juejin.im/post/5ce574696fb9a07ef90c7121

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

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

Installing

  1. go get github.com/google/wire/cmd/wire

Quick Start

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

  1. $ ls
  2. main.go wire.go

main.go

  1. package main
  2. import "fmt"
  3. type Message struct {
  4. msg string
  5. }
  6. type Greeter struct {
  7. Message Message
  8. }
  9. type Event struct {
  10. Greeter Greeter
  11. }
  12. // NewMessage Message的构造函数
  13. func NewMessage(msg string) Message {
  14. return Message{
  15. msg:msg,
  16. }
  17. }
  18. // NewGreeter Greeter构造函数
  19. func NewGreeter(m Message) Greeter {
  20. return Greeter{Message: m}
  21. }
  22. // NewEvent Event构造函数
  23. func NewEvent(g Greeter) Event {
  24. return Event{Greeter: g}
  25. }
  26. func (e Event) Start() {
  27. msg := e.Greeter.Greet()
  28. fmt.Println(msg)
  29. }
  30. func (g Greeter) Greet() Message {
  31. return g.Message
  32. }
  33. // 使用wire前
  34. func main() {
  35. message := NewMessage("hello world")
  36. greeter := NewGreeter(message)
  37. event := NewEvent(greeter)
  38. event.Start()
  39. }
  40. /*
  41. // 使用wire后
  42. func main() {
  43. event := InitializeEvent("hello_world")
  44. event.Start()
  45. }*/

wire.go

  1. // +build wireinject
  2. // The build tag makes sure the stub is not built in the final build.
  3. package main
  4. import "github.com/google/wire"
  5. // InitializeEvent 声明injector的函数签名
  6. func InitializeEvent(msg string) Event{
  7. wire.Build(NewEvent, NewGreeter, NewMessage)
  8. return Event{} //返回值没有实际意义,只需符合函数签名即可
  9. }

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

  1. $ wire
  2. wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
  3. $ ls
  4. main.go wire.go wire_gen.go

wire_gen.go wire生成的文件

  1. // Code generated by Wire. DO NOT EDIT.
  2. //go:generate wire
  3. //+build !wireinject
  4. package main
  5. // Injectors from wire.go:
  6. func InitializeEvent(msg string) Event {
  7. message := NewMessage(msg)
  8. greeter := NewGreeter(message)
  9. event := NewEvent(greeter)
  10. return event
  11. }

使用前 V.S 使用后

  1. ...
  2. /*
  3. // 使用wire前
  4. func main() {
  5. message := NewMessage("hello world")
  6. greeter := NewGreeter(message)
  7. event := NewEvent(greeter)
  8. event.Start()
  9. }*/
  10. // 使用wire后
  11. func main() {
  12. event := InitializeEvent("hello_world")
  13. event.Start()
  14. }
  15. ...

使用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的函数签名,注意要在文件第一行加上

  1. // +build wireinject
  2. ...

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

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

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

高级特性

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

接口绑定

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

抽象成接口依赖更有助于单元测试哦!
搞定Go单元测试(一)——基础原理
搞定Go单元测试(二)—— mock框架(gomock)

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

  1. // UserService
  2. type UserService struct {
  3. userRepo UserRepository // <-- UserService依赖UserRepository接口
  4. }
  5. // UserRepository 存放User对象的数据仓库接口,比如可以是mysql,restful api ....
  6. type UserRepository interface {
  7. // GetUserByID 根据ID获取User, 如果找不到User返回对应错误信息
  8. GetUserByID(id int) (*User, error)
  9. }
  10. // NewUserService *UserService构造函数
  11. func NewUserService(userRepo UserRepository) *UserService {
  12. return &UserService{
  13. userRepo:userRepo,
  14. }
  15. }
  16. // mockUserRepo 模拟一个UserRepository实现
  17. type mockUserRepo struct {
  18. foo string
  19. bar int
  20. }
  21. // GetUserByID UserRepository接口实现
  22. func (u *mockUserRepo) GetUserByID(id int) (*User,error){
  23. return &User{}, nil
  24. }
  25. // NewMockUserRepo *mockUserRepo构造函数
  26. func NewMockUserRepo(foo string,bar int) *mockUserRepo {
  27. return &mockUserRepo{
  28. foo:foo,
  29. bar:bar,
  30. }
  31. }
  32. // MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
  33. var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

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

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

定义injector函数签名:

  1. ...
  2. func InitializeUserService(foo string, bar int) *UserService{
  3. wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
  4. return nil
  5. }
  6. ...

示例传送门: binding-interfaces

返回错误

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

  1. // Config 配置
  2. type Config struct {
  3. // RemoteAddr 连接的远程地址
  4. RemoteAddr string
  5. }
  6. // APIClient API客户端
  7. type APIClient struct {
  8. c Config
  9. }
  10. // NewAPIClient APIClient构造函数,如果入参校验失败,返回错误原因
  11. func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二个参数设置成error
  12. if c.RemoteAddr == "" {
  13. return nil, errors.New("没有设置远程地址")
  14. }
  15. return &APIClient{
  16. c:c,
  17. },nil
  18. }
  19. // Service
  20. type Service struct {
  21. client *APIClient
  22. }
  23. // NewService Service构造函数
  24. func NewService(client *APIClient) *Service{
  25. return &Service{
  26. client:client,
  27. }
  28. }

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

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

观察一下wire生成的injector

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

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

示例传送门: return-error

Cleanup functions

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

  1. // FileReader
  2. type FileReader struct {
  3. f *os.File
  4. }
  5. // NewFileReader *FileReader 构造函数,第二个参数是cleanup function
  6. func NewFileReader(filePath string) (*FileReader, func(), error){
  7. f, err := os.Open(filePath)
  8. if err != nil {
  9. return nil,nil,err
  10. }
  11. fr := &FileReader{
  12. f:f,
  13. }
  14. fn := func() {
  15. log.Println("cleanup")
  16. fr.f.Close()
  17. }
  18. return fr,fn,nil
  19. }

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

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

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

示例传送门: cleanup-functions

Provider set

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

  1. // NewMessage Message的构造函数
  2. func NewMessage(msg string) Message {
  3. return Message{
  4. msg:msg,
  5. }
  6. }
  7. // NewGreeter Greeter构造函数
  8. func NewGreeter(m Message) Greeter {
  9. return Greeter{Message: m}
  10. }
  11. // NewEvent Event构造函数
  12. func NewEvent(g Greeter) Event {
  13. return Event{Greeter: g}
  14. }
  15. func (e Event) Start() {
  16. msg := e.Greeter.Greet()
  17. fmt.Println(msg)
  18. }
  19. // EventSet Event通常是一起使用的一个集合,使用wire.NewSet进行组合
  20. var EventSet = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--

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

  1. func InitializeEvent(msg string) Event{
  2. //wire.Build(NewEvent, NewGreeter, NewMessage)
  3. wire.Build(EventSet)
  4. return Event{}
  5. }

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

示例传送门: provider-set

结构体provider

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

  1. type Foo int
  2. type Bar int
  3. func ProvideFoo() Foo {
  4. return 1
  5. }
  6. func ProvideBar() Bar {
  7. return 2
  8. }
  9. type FooBar struct {
  10. MyFoo Foo
  11. MyBar Bar
  12. }
  13. var Set = wire.NewSet(
  14. ProvideFoo,
  15. ProvideBar,
  16. wire.Struct(new(FooBar), "MyFoo", "MyBar"))

通过wire.Struct来指定那些字段要被注入到结构体中,如果是全部字段,也可以简写成:

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

生成的injector函数:

  1. func InitializeFooBar() FooBar {
  2. foo := ProvideFoo()
  3. bar := ProvideBar()
  4. fooBar := FooBar{
  5. MyFoo: foo,
  6. MyBar: bar,
  7. }
  8. return fooBar
  9. }

示例传送门: struct-provider

Best Practices

区分类型

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

  1. type FooBar struct {
  2. foo string
  3. bar string
  4. }
  5. func NewFooBar(foo string, bar string) FooBar {
  6. return FooBar{
  7. foo: foo,
  8. bar: bar,
  9. }
  10. }

injector函数签名定义:

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

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

  1. provider has multiple parameters of type string

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

  1. type Foo string
  2. type Bar string
  3. type FooBar struct {
  4. foo Foo
  5. bar Bar
  6. }
  7. func NewFooBar(foo Foo, bar Bar) FooBar {
  8. return FooBar{
  9. foo: foo,
  10. bar: bar,
  11. }
  12. }

injector函数签名定义:

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

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

  1. type MySQLConnectionString string
  2. type FileReader io.Reader

示例传送门 distinguishing-types

Options Structs

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

  1. type Message string
  2. // Options
  3. type Options struct {
  4. Messages []Message
  5. Writer io.Writer
  6. Reader io.Reader
  7. }
  8. type Greeter struct {
  9. }
  10. // NewGreeter Greeter的provider方法使用Options以避免构造函数过长
  11. func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
  12. return nil, nil
  13. }
  14. // GreeterSet 使用wire.Struct设置Options为provider
  15. var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)

injector函数签名:

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

示例传送门 options-structs

一些缺点和限制

额外的类型定义

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

mock支持暂时不够友好

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

更多参考

ft_authoradmin  ft_create_time2019-11-19 13:54
 ft_update_time2019-11-19 16:56