精通Wire

警告
本文最后更新于 2020-10-14,文中内容可能已过时。

Wire是Go Cloud团队开发的Golang中的轻量级依赖注入工具。它会自动生成代码,然后在编译时注入依赖项。

依赖注入是保持软件“loose-coupling and easy to maintain(松耦合且易于维护)”的最重要的设计原则之一。

该原理已在所有类型的开发平台中广泛使用,并且有许多与之相关的出色工具。

在所有这些工具中,最著名的是Spring和Spring IOC,该框架的基础对其今天的主导地位起着决定性作用。

实际上,S.O.L.I.D软件开发原则中的“ D”是专门针对此主题的。

依赖注入是如此重要,以至于Golang社区中已经有很多解决方案,例如从Uber的dig和从Facebook的inject。它们都通过反射机制实现了运行时依赖项注入。

Go Cloud团队为什么要彻底改变方向?因为他们认为上述所有库都不符合Go的哲学:

Clear is better than clever Reflection is never clear. 清晰胜于巧妙,反射永远不会清晰。 — Rob Pike

作为代码生成工具,Wire可以生成源代码并在编译时实现依赖项注入。它不需要反射或Service Locators。如您将在后面看到的,Wire生成的代码与手工编写的代码没有区别。这种方法带来一些好处:

  1. 轻松调试。如果缺少任何依赖项,则在编译过程中将报告错误
  2. 由于不需要service locators,因此对命名没有特殊要求
  3. 避免依赖膨胀。生成的代码将仅导入您需要的依赖项,而运行时依赖项注入直到运行时才能识别未使用的依赖项。
  4. 依赖图静态存储在源代码中,这使得工具和可视化变得更加容易

有关设计Wire的详细折衷方法,请访问Go Blog

尽管Wire的最新版本仅为v0.4.0,但它已经达到了团队设定的目标并且相当成熟。预计将来不会有重大变化。从团队的声明中可以看出:

It works well for the tasks it was designed to perform, and we prefer to keep it as simple as possible._ We’ll not be accepting new features at this time, but will gladly accept bug reports and fixes._ —Wire team

安装wire非常容易,只需运行

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

您将在$GOPATH/bin上安装Wire命令行工具,确保$GOPATH/bin$PATH中,然后可以在任何目录下运行wire。

在继续之前,我们需要在Wire中解释两个核心概念:ProviderInjector

Provider:用于创建组件的普通函数。这些方法将所需的依赖项作为参数,创建一个组件并返回它。 组件可以是对象或函数,实际上它可以是任何类型。唯一的限制是:one type can only have a single provider in the entire dependency graph。因此,返回int的提供程序不是一个好主意。在这种情况下,您可以通过定义类型别名来解决它。例如,首先定义type Category int,然后让提供者返回Category类型

以下是典型的provider示例:

1
2
3
4
5
6
// DefaultConnectionOpt provide default connection option
func DefaultConnectionOpt()*ConnectionOpt{...}
// NewDb provide an Db object
func NewDb(opt *ConnectionOpt)(*Db, error){...}
// NewUserLoadFunc provide a function which can load user
func NewUserLoadFunc(db *Db)(func(int) *User, error){...}

实际上,通常将一组相关的提供程序放在一起并组织到ProviderSet中,以方便维护和切换。

1
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

Injector:由wire生成的函数,以依赖关系顺序调用提供程序。 为了生成injector,我们在wire.go中定义了 the injector function signature (文件名不是必需的,但通常是这种情况)。然后调用wire.build in the function body with provider as parameter (与顺序无关)。

由于wire.go中的函数并未真正返回值,因此为了避免编译器错误,只需简单的用panic包裹他们。不用担心运行时错误,因为它实际上不会执行,所以只是生成实际代码的提示。一个简单的wire.go示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// +build wireinject

package main

import "github.com/google/wire"

func UserLoader()(func(int)*User, error){
   panic(wire.Build(NewUserLoadFunc, DbSet))
}

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

完成此代码后,运行命令行wire将生成文件wire_gen.go,其中包含 injector function 的实现。 wire.go中的任何非注入器代码都将照原样复制到wire_gen.go(尽管在技术上允许,但不建议这样做)。生成的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
   "github.com/google/wire"
)

// Injectors from wire.go:
func UserLoader() (func(int) *User, error) {
   connectionOpt := DefaultConnectionOpt()
   db, err := NewDb(connectionOpt)
   if err != nil {
      return nil, err
   }
   v, err := NewUserLoadFunc(db)
   if err != nil {
      return nil, err
   }
   return v, nil
}

// wire.go:
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

上面的代码有两个有趣的地方:

  1. wire.go 的第一行//+build wireinject,此构建标记可确保在常规编译期间忽略wire.go文件(因为在常规编译期间未指定wireinject标记)。与之相反,wire_gen.go的第四行//+build! Wireinject。这两套相对的构建标记可确保在任何情况下,wire.go和wire_gen.go文件只有一个生效,避免了编译错误“function UserLoader has been defined”
  2. 自动生成的函数UserLoader包含错误处理。它几乎与手写代码相同。对于这样一个简单的初始化过程,手写是可以接受的,但是当组件的数量达到数十,数百甚至更多时,将显示出自动生成的优点。

有两种方法可以触发“生成”操作:go generatewire

前者仅在wire_gen.go已经存在的情况下才有效(因为wire_gen.go的第三行//go:generate wire)。

虽然后者可以随时执行。后者支持更多参数来微调生成行为,因此建议始终使用wire命令。

然后我们可以使用real injector,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "log"

func main() {
   fn, err := UserLoader()
   if err != nil {
      log.Fatal(err)
   }
   user := fn(123)
   ...
}

如果您不小心忘记了a certain provider,wire将报告特定错误,以帮助开发人员快速解决问题。例如,我们修改wire.go以删除NewDb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// +build wireinject

package main

import "github.com/google/wire"

func UserLoader()(func(int)*User, error){
   panic(wire.Build(NewUserLoadFunc, DbSet))
}

var DbSet = wire.NewSet(DefaultConnectionOpt) //forgot add Db provider

执行wire命令,然后将报告一个显式错误:“no provider found for * example.Db”

1
2
3
4
wire: /usr/example/wire.go:7:1: inject UserLoader: no provider found for *example.Db
needed by func(int) *example.User in provider "NewUserLoadFunc" (/usr/example/provider.go:24:6)
wire: example: generate failed
wire: at least one generate failure

同样,如果在wire.go中编写了未使用的提供程序,则将出现明确的错误消息。

在讨论基本用法之后,让我们看一下高级功能

有时我们需要注入一个接口。有两种选择:

  1. 直接的方法是在provider中创建一个类,然后返回接口类型。但这不符合Go最佳实践。不建议
  2. 让provider返回类,然后在注入器中声明一个接口绑定,例如:
1
2
3
4
5
6
7
// FooInf, an interface
// FooClass, an class which implements FooInf
// fooClassProvider, a provider function that provider *FooClassvar
set = wire.NewSet(
    fooClassProvider,
    wire.Bind(new(FooInf), new(*FooClass) // bind class to interface
)

有时我们不需要任何特定的初始化工作,我们只需创建一个struct实例,为指定的字段分配一个值,然后返回即可。当字段很多时,这种工作可能会很乏味。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// provider.go
type App struct {
    Foo *Foo
    Bar *Bar
}

func DefaultApp(foo *Foo, bar *Bar)*App{
    return &App{Foo: foo, Bar: bar}
}
// wire.go
...
wire.Build(provideFoo, provideBar, DefaultApp)
...

在这种情况下,wire.Struct可以通过指定字段名称来注入字段:

1
wire.Build(provideFoo, provideBar, wire.Struct(new(App),"Foo","Bar")

如果要注入所有字段,则有一种更简化的方法:

1
wire.Build(provideFoo, provideBar, wire.Struct(new(App), "*")

如果要忽略结构中的某些字段,则可以修改结构定义:

1
2
3
4
5
type App struct {
    Foo *Foo
    Bar *Bar
    NoInject int `wire:"-"`
}

然后NoInject将被忽略。与 regular providers 相比,wire.Struct提供了额外的灵活性:它可以适应指针和非指针类型,并根据需要自动调整生成的代码。

wire.Struct确实提供了一些便利。但这要求注入的字段必须是可公开访问的,这会导致结构公开可能会隐藏的细节。

幸运的是,可以通过上述“绑定接口”解决此问题。使用wire.Struct构造一个对象并将类绑定到接口。至于如何在便利性和封装之间进行权衡,则取决于您的具体情况。

有时您需要将值绑定到字段。在这种情况下,您可以使用wire.Value

1
2
3
4
5
6
7
// provider.go
type Foo struct {
    X int
}// wire.go
...
wire.Build(wire.Value(Foo{X: 42}))
...

对于接口值,请使用wire.InterfaceValue

1
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))

有时我们需要使用结构的字段作为提供者,例如:

1
2
3
4
5
6
7
8
// provider
func provideBar(foo Foo)*Bar{
    return foo.Bar
}
// injector
...
wire.Build(provideFoo, provideBar)
...

在这种情况下,您可以使用wire.FieldsOf简化它,避免繁琐的提供程序定义:

1
wire.Build(provideFoo, wire.FieldsOf(new(Foo), Bar))

wire.Struct类似,wire.FieldsOf也可以自动适应指针/非指针注入请求

如前所述,如果provider and injector函数返回错误,Wire将自动处理它们。此外,Wire还具有另一种自动处理功能:cleanup functions。

所谓的cleanup function是指带有签名func()的闭包。它从提供程序返回以确保可以清除在提供程序中分配的资源。

cleanup function的典型应用场景是文件资源和网络连接资源管理,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type App struct {
   File *os.File
   Conn net.Conn
}

func provideFile() (*os.File, func(), error) {
   f, err := os.Open("foo.txt")
   if err != nil {
      return nil, nil, err
   }
   cleanup := func() {
      if err := f.Close(); err != nil {
         log.Println(err)
      }
   }
   return f, cleanup, nil
}

func provideNetConn() (net.Conn, func(), error) {
   conn, err := net.Dial("tcp", "foo.com:80")
   if err != nil {
      return nil, nil, err
   }
   cleanup := func() {
      if err := conn.Close(); err != nil {
         log.Println(err)
      }
   }
   return conn, cleanup, nil
}

上面的代码定义了两个提供程序,分别提供文件资源和网络连接资源。 wire.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// +build wireinject

package main

import "github.com/google/wire"

func NewApp() (*App, func(), error) {
   panic(wire.Build(
      provideFile,
      provideNetConn,
      wire.Struct(new(App), "*"),
   ))
}

请注意,因为提供程序返回了a cleanup function,所以注入器函数还必须返回它,否则将发生错误

wire_gen.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func NewApp() (*App, func(), error) {
   file, cleanup, err := provideFile()
   if err != nil {
      return nil, nil, err
   }
   conn, cleanup2, err := provideNetConn()
   if err != nil {
      cleanup()
      return nil, nil, err
   }
   app := &App{
      File: file,
      Conn: conn,
   }
   return app, func() {
      cleanup2()
      cleanup()
   }, nil
}

生成的代码中有两点值得注意:

  1. ProvideNetConn失败时将调用cleanup(),以确保后续处理错误不会影响先前分配的资源的清除。
  2. 返回的最终闭包自动组合cleanup2()cleanup()。这意味着无论分配多少资源,只要调用过程成功,它们的清理工作都将在一个清理函数中进行处理。The caller of the injector will be responsible for the final cleanup

可以想象,有很多 cleaning functions 组合在一起时,手动处理以上两个问题非常繁琐且容易出错。 Wire的优势再次得到体现。

然后我们可以使用它:

1
2
3
4
5
6
7
8
func main() {
   app, cleanup, err := NewApp()
   if err != nil {
      log.Fatal(err)
   }
   defer cleanup()
   ...
}

请注意defer cleanup(),它确保最终回收所有资源。


Mastering Wire

相关内容