wire是 Google 开源的一个依赖注入工具。它是一个代码生成器,并不是一个框架。我们只需要在一个特殊的go文件中告诉wire类型之间的依赖关系,它会自动帮我们生成代码,帮助我们创建指定类型的对象,并组装它的依赖。

依赖注入很重要,所以Golang社区中早已有人开发了相关工具, 比如来自Uber 的 dig 、来自Facebook 的 inject 。他们都通过反射机制实现了运行时依赖注入。

为什么Go Cloud团队还要重造一遍轮子呢? 因为在他们看来上述类库都不符合Go的哲学:

Clear is better than clever ,Reflection is never clear. — Rob Pike

作为一个代码生成工具, Wire可以生成Go源码并在编译期完成依赖注入。 它不需要反射机制或 Service Locators 。 后面会看到, Wire 生成的代码与手写无异。 这种方式带来一系列好处:

  1. 方便debug,若有依赖缺失编译时会报错
  2. 因为不需要 Service Locators, 所以对命名没有特殊要求
  3. 避免依赖膨胀。 生成的代码只包含被依赖的代码,而运行时依赖注入则无法作到这一点
  4. 依赖关系静态存于源码之中, 便于工具分析与可视化

快速使用

先安装工具:

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

上面的命令会在$GOPATH/bin中生成一个可执行程序wire,这就是代码生成器。我个人习惯把$GOPATH/bin加入系统环境变量$PATH中,所以可直接在命令行中执行wire命令。

下面我们在一个例子中看看如何使用wire。

现在,我们来到一个黑暗的世界,这个世界中有一个邪恶的怪兽。我们用下面的结构表示,同时编写一个创建方法:

1
2
3
4
5
6
7
type Monster struct {
  Name string
}

func NewMonster() Monster {
  return Monster{Name: "kitty"}
}

有怪兽肯定就有勇士,结构如下,同样地它也有创建方法:

1
2
3
4
5
6
7
type Player struct {
  Name string
}

func NewPlayer(name string) Player {
  return Player{Name: name}
}

终于有一天,勇士完成了他的使命,战胜了怪兽:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Mission struct {
  Player  Player
  Monster Monster
}

func NewMission(p Player, m Monster) Mission {
  return Mission{p, m}
}

func (m Mission) Start() {
  fmt.Printf("%s defeats %s, world peace!\n", m.Player.Name, m.Monster.Name)
}

这可能是某个游戏里面的场景哈,我们看如何将上面的结构组装起来放在一个应用程序中:

1
2
3
4
5
6
7
func main() {
  monster := NewMonster()
  player := NewPlayer("dj")
  mission := NewMission(player, monster)

  mission.Start()
}

代码量少,结构不复杂的情况下,上面的实现方式确实没什么问题。但是项目庞大到一定程度,结构之间的关系变得非常复杂的时候,这种手动创建每个依赖,然后将它们组装起来的方式就会变得异常繁琐,并且容易出错。这个时候勇士wire出现了!

wire的要求很简单,新建一个wire.go文件(文件名可以随意),创建我们的初始化函数。比如,我们要创建并初始化一个Mission对象,我们就可以这样:

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

package main

import "github.com/google/wire"

func InitMission(name string) Mission {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}
}

首先这个函数的返回值就是我们需要创建的对象类型,wire只需要知道类型,return后返回什么不重要。然后在函数中,我们调用wire.Build()将创建Mission所依赖的类型的构造器传进去。例如,需要调用NewMission()创建Mission类型,NewMission()接受两个参数一个Monster类型,一个Player类型。Monster类型对象需要调用NewMonster()创建,Player类型对象需要调用NewPlayer()创建。所以NewMonster()和NewPlayer()我们也需要传给wire。

由于wire.go中的函数并没有真正返回值,为避免编译器报错, 简单地用panic函数包装起来即可。不用担心执行时报错, 因为它不会实际运行,只是用来生成真正的代码的依据。一个简单的wire.go 示例

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

package main

import "github.com/google/wire"

func InitMission(name string) Mission {
 panic(wire.Build(NewMonster, NewPlayer, NewMission))
}

文件编写完成之后,执行wire命令:

1
2
3
$ wire
wire: github.com/darjun/go-daily-lib/wire/get-started/after: \
wrote D:\code\golang\src\github.com\darjun\go-daily-lib\wire\get-started\after\wire_gen.go

我们看看生成的wire_gen.go文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Code generated by Wire. DO NOT EDIT.

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

package main

// Injectors from wire.go:

func InitMission(name string) Mission {
  player := NewPlayer(name)
  monster := NewMonster()
  mission := NewMission(player, monster)
  return mission
}

这个InitMission()函数是不是和我们在main.go中编写的代码一样!wire.go 中若有非injector 的代码将被原样复制到 wire_gen.go 中(虽然技术上允许,但不推荐这样作)。

接下来,我们可以直接在main.go调用InitMission():

1
2
3
4
5
func main() {
  mission := InitMission("dj")

  mission.Start()
}

wire.go 第一行 // +build wireinject ,这个 build tag 确保在常规编译时忽略wire.go 文件(因为常规编译时不会指定 wireinject 标签)。 与之相对的是 wire_gen.go 中的 //+build !wireinject 。两组对立的build tag保证在任意情况下, wire.go 与 wire_gen.go 只有一个文件生效, 避免了“UserLoader方法被重复定义”的编译错误

由于现在是两个文件,我们不能用go run main.go运行程序,可以用go run .运行。运行结果与之前的例子一模一样!

注意,如果你运行时,出现了InitMission重定义,那么检查一下你的//+build wireinject与package main这两行之间是否有空行,这个空行必须要有!见https://github.com/google/wire/iss`ues/117

要触发“生成”动作有两种方式:go generate 或 wire 。前者仅在 wire_gen.go 已存在的情况下有效(因为wire_gen.go 的第三行 //go:generate wire),而后者在任何时候都有可以调用。 并且后者有更多参数可以对生成动作进行微调, 所以建议始终使用 wire 命令。

在生成wire_gen.go的时候,如果已经生成了文件,并且本次没有新增Initialize函数,那么可以直接使用wire_gen.go 中的go generate进行重新生成。但是如果新增加了一个类的注入过程,应该使用wire命令重新生成wire_gen.go文件,注意要把// +build wireinject注释掉,否则找不到对应的类。

基础概念

wire有两个基础概念,Provider(构造器)和Injector(注入器)。Provider实际上就是创建函数,大家意会一下。我们上面InitMission就是Injector。每个注入器实际上就是一个对象的创建和初始化函数。在这个函数中,我们只需要告诉wire要创建什么类型的对象,这个类型的依赖,wire工具会为我们生成一个函数完成对象的创建和初始化工作。

Wire 中的主要机制是 providers: 一个可以产生值的函数。这些函数是普通的 Go 代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
    return Foo{X: 42}
}

Provider 函数必须导出,以便从其他包中使用,就像普通函数一样。

Provider 可以使用参数指定依赖项:

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

// ...

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

Providers 可以返回错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package foobarbaz

import (
    "context"
    "errors"
)

// ...

type Baz struct {
    X int
}

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("cannot provide baz when bar is zero")
    }
    return Baz{X: bar.X}, nil
}

Providers 可以分组到提 provider sets 中。如果几个 provider 经常一起使用,这很有用。要将这些提供程序添加到名为 SuperSet 的新集中,请使用 wire.NewSet 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package foobarbaz

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

// ...

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

您也可以将其他 provider sets 加到 provider set 中。

应用程序使用 injector 连接这些 provider:按依赖顺序调用 provider 的函数。使用 Wire,写下 injector’s 的签名,然后 Wire 生成函数的主体。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

像 provider 一样,injectors 可以在输入上参数化(然后发送给 provider),并且可以返回错误。wire.Build 参数与 wire.NewSet 一样:他们构成一个 provider 集合。这是在该 injector 注入器的生成代码期间使用的 provider 集。

在 injectors 的文件中发现的任何非 injector 声明将被复制到生成的文件中。

你可以在包目录中调用Wire来生成 injector:

1
wire

Wire 将产生一个实现 injector 调用方法名为 wire_gen.go 文件。看起来如下:

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

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

package main

import (
    "example.com/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    foo := foobarbaz.ProvideFoo()
    bar := foobarbaz.ProvideBar(foo)
    baz, err := foobarbaz.ProvideBaz(ctx, bar)
    if err != nil {
        return 0, err
    }
    return baz, nil
}

就像你看到的一样,输出非常接近开发人员自己编写的内容。此外,运行时对 Wire 的依赖性很小:所有编写的代码都只是普通的 Go 代码,可以在没有 Wire 的情况下使用。

创建 wire_gen.go 后,您可以通过运行 go generate 来重新生成它。

参数

同样细心的你应该发现了,我们上面编写的InitMission()函数带有一个string类型的参数。并且在生成的InitMission()函数中,这个参数传给了NewPlayer()。NewPlayer()需要string类型的参数,而参数类型就是string。所以生成的InitMission()函数中,这个参数就被传给了NewPlayer()。如果我们让NewMonster()也接受一个string参数呢?

1
2
3
func NewMonster(name string) Monster {
  return Monster{Name: name}
}

那么生成的InitMission()函数中NewPlayer()和NewMonster()都会得到这个参数:

1
2
3
4
5
6
func InitMission(name string) Mission {
  player := NewPlayer(name)
  monster := NewMonster(name)
  mission := NewMission(player, monster)
  return mission
}

实际上,wire在生成代码时,构造器需要的参数(或者叫依赖)会从参数中查找或通过其它构造器生成。决定选择哪个参数或构造器完全根据类型。如果参数或构造器生成的对象有类型相同的情况,运行wire工具时会报错。如果我们想要定制创建行为,就需要为不同类型创建不同的参数结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type PlayerParam string
type MonsterParam string

func NewPlayer(name PlayerParam) Player {
  return Player{Name: string(name)}
}

func NewMonster(name MonsterParam) Monster {
  return Monster{Name: string(name)}
}

func main() {
  mission := InitMission("dj", "kitty")
  mission.Start()
}

// wire.go
func InitMission(p PlayerParam, m MonsterParam) Mission {
  wire.Build(NewPlayer, NewMonster, NewMission)
  return Mission{}
}

生成的代码如下:

1
2
3
4
5
6
func InitMission(m MonsterParam, p PlayerParam) Mission {
  player := NewPlayer(p)
  monster := NewMonster(m)
  mission := NewMission(player, monster)
  return mission
}

在参数比较复杂的时候,建议将参数放在一个结构中。

错误

不是所有的构造操作都能成功,没准勇士出山前就死于小人之手:

1
2
3
4
5
6
func NewPlayer(name string) (Player, error) {
  if time.Now().Unix()%2 == 0 {
    return Player{}, errors.New("player dead")
  }
  return Player{Name: name}, nil
}

我们使创建随机失败,修改注入器InitMission()的签名,增加error返回值:

1
2
3
4
func InitMission(name string) (Mission, error) {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}, nil
}

生成的代码,会将NewPlayer()返回的错误,作为InitMission()的返回值:

1
2
3
4
5
6
7
8
9
func InitMission(name string) (Mission, error) {
  player, err := NewPlayer(name)
  if err != nil {
    return Mission{}, err
  }
  monster := NewMonster()
  mission := NewMission(player, monster)
  return mission, nil
}

wire遵循fail-fast的原则,错误必须被处理。如果我们的注入器不返回错误,但构造器返回错误,wire工具会报错!

panic语法

如果你厌倦了在 injector 函数声明的末尾写 return foobarbaz.Foo{}, nil,可以使用 panic 用一个简洁的方式写出来:

1
2
3
func injectFoo() Foo {
    panic(wire.Build(/* ... */))
}

高级特性

下面简单介绍一下wire的高级特性。

ProviderSet

有时候可能多个类型有相同的依赖,我们每次都将相同的构造器传给wire.Build()不仅繁琐,而且不易维护,一个依赖修改了,所有传入wire.Build()的地方都要修改。为此,wire提供了一个ProviderSet(构造器集合),可以将多个构造器打包成一个集合,后续只需要使用这个集合即可。假设,我们有关勇士和怪兽的故事有两个结局:

 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
type EndingA struct {
  Player  Player
  Monster Monster
}

func NewEndingA(p Player, m Monster) EndingA {
  return EndingA{p, m}
}

func (p EndingA) Appear() {
  fmt.Printf("%s defeats %s, world peace!\n", p.Player.Name, p.Monster.Name)
}

type EndingB struct {
  Player  Player
  Monster Monster
}

func NewEndingB(p Player, m Monster) EndingB {
  return EndingB{p, m}
}

func (p EndingB) Appear() {
  fmt.Printf("%s defeats %s, but become monster, world darker!\n", p.Player.Name, p.Monster.Name)
}

编写两个注入器:

1
2
3
4
5
6
7
8
9
func InitEndingA(name string) EndingA {
  wire.Build(NewMonster, NewPlayer, NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(NewMonster, NewPlayer, NewEndingB)
  return EndingB{}
}

我们观察到两次调用wire.Build()都需要传入NewMonster和NewPlayer。两个还好,如果很多的话写起来就麻烦了,而且修改也不容易。这种情况下,我们可以先定义一个ProviderSet:

1
var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

后续直接使用这个set:

1
2
3
4
5
6
7
8
9
func InitEndingA(name string) EndingA {
  wire.Build(monsterPlayerSet, NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(monsterPlayerSet, NewEndingB)
  return EndingB{}
}

而后如果要添加或删除某个构造器,直接修改set的定义处即可。

接口注入

依赖注入通常用于绑定接口的具体实现。Wire 通过类型标识将输入与输出匹配,因此倾向于创建一个返回接口类型的程序。然而,这并不是惯用的,因为 Go 的最佳实践是返回具体类型。相反,您可以在提供程序 providers sets 中声明接口绑定:

 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
type Fooer interface {
    Foo() string
}

type MyFooer string

func (b *MyFooer) Foo() string {
    return string(*b)
}

func provideMyFooer() *MyFooer {
    b := new(MyFooer)
    *b = "Hello, World!"
    return b
}

type Bar string

func provideBar(f Fooer) string {
    // f will be a *MyFooer.
    return f.Foo()
}

var Set = wire.NewSet(
    provideMyFooer,
    wire.Bind(new(Fooer), new(*MyFooer)),
    provideBar)

wire.Bind 的第一个参数是指向所需接口类型值的指针,任何包含接口绑定的集合都必须在提供具体类型的同一集合中有一个 provides 程序。

如果需要,还可以将一个接口绑定到另一个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type FooerPlus interface {
  Fooer
  Bar() String
}

func ProvideFooerPlus() FooerPlus {
  ...
}

var FooerPlusAsFooer = wire.NewSet(
  ProvideFooerPlus,
  wire.Bind(new(Fooer), *new(FooerPlus)))

属性自动注入

有时我们不需什么特定的初始化工作, 只是简单地创建一个对象实例, 为其指定属性赋值,然后返回。当属性多的时候,这种工作会很无聊。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 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), "*")

如果struct 中有个别属性不想被注入,那么可以修改 struct 定义:

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

这时 NoInject 属性会被忽略。与常规 provider相比, wire.Struct 提供一项额外的灵活性: 它能适应指针与非指针类型,根据需要自动调整生成的代码。

大家可以看到wire.Struct的确提供了一些便利。但它要求注入属性可公开访问, 这导致对象暴露本可隐藏的细节。

好在这个问题可以通过上面提到的“接口注入”来解决。用 wire.Struct 创建对象,然后将其类绑定到接口上。 至于在实践中如何权衡便利性和封装程度,则要具体情况具体分析了。

绑定值

有时候,我们需要为某个类型绑定一个值,而不想依赖构造器每次都创建一个新的值。有些类型天生就是单例,例如配置,数据库对象(sql.DB)。这时我们可以使用wire.Value绑定值,使用wire.InterfaceValue绑定接口。例如,我们的怪兽一直是一个Kitty,我们就不用每次都去创建它了,直接绑定这个值就 ok 了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var kitty = Monster{Name: "kitty"}

func InitEndingA(name string) EndingA {
  wire.Build(NewPlayer, wire.Value(kitty), NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(NewPlayer, wire.Value(kitty), NewEndingB)
  return EndingB{}
}

注意一点,这个值每次使用时都会拷贝,需要确保拷贝无副作用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// wire_gen.go
func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
monster :=_wireMonsterValue
  endingA := NewEndingA(player, monster)
  return endingA
}

var (
  _wireMonsterValue = kitty
)

为接口类型绑定具体值,可以使用 wire.InterfaceValue :

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

把对象属性用作Provider

有时我们只是需要用某个对象的属性作为Provider,例如

1
2
3
4
5
6
7
8
9
// provider
func provideBar(foo Foo)*Bar{
    return foo.Bar
}

// injector
...
wire.Build(provideFoo, provideBar)
...

这时可以用 wire.FieldsOf 加以简化,省掉啰嗦的 provider:

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

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

现在我们直接创建了Mission结构,如果想获得Monster和Player类型的对象,就可以对Mission使用wire.FieldsOf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func NewMission() Mission {
  p := Player{Name: "dj"}
  m := Monster{Name: "kitty"}

  return Mission{p, m}
}

// wire.go
func InitPlayer() Player {
  wire.Build(NewMission, wire.FieldsOf(new(Mission), "Player"))
}

func InitMonster() Monster {
  wire.Build(NewMission, wire.FieldsOf(new(Mission), "Monster"))
}

// main.go
func main() {
  p := InitPlayer()
  fmt.Println(p.Name)
}

同样的,第一个参数为new(结构名),后面跟多个参数表示将哪些字段作为构造器,*表示全部。

清理函数

前面提到若provider 和 injector 函数有返回错误, 那么wire会自动处理。除此以外,wire还有另一项自动处理能力:清理函数。

所谓清理函数是指型如 func() 的闭包, 它随provider生成的组件一起返回, 确保组件所需资源可以得到清理.

清理函数典型的应用场景是文件资源和网络连接资源,例如:

 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
}

上述代码定义了两个 provider 分别提供了文件资源和网络连接资源

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), "*"),
   ))
}

注意由于provider 返回了清理函数, 因此injector函数签名也必须返回,否则将会报错

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
}

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

  • 当 provideNetConn 出错时会调用 cleanup() , 这确保了即使后续处理出错也不会影响前面已分配资源的清理。
  • 最后返回的闭包自动组合了 cleanup2() 和 cleanup() 。 意味着无论分配了多少资源, 只要调用过程不出错,他们的清理工作就会被集中到统一的清理函数中。 最终的清理工作由injector的调用者负责

可以想像当几十个清理函数的组合在一起时, 手工处理上述两个场景是非常繁琐且容易出错的。 wire 的优势再次得以体现。

然后就可以使用了:

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

注意main函数中的 defer cleanup() ,它确保了所有资源最终得到回收

参考: Go 每日一库之 wire 一文读懂Wire