Compile-time Dependency Injection With Go Cloud's Wire 编译时依赖注入 运行时依赖注入

Go 每日一库之 wire - SegmentFault 思否 https://segmentfault.com/a/1190000021895220

这个InitMission()函数是不是和我们在main.go中编写的代码一毛一样!接下来,我们可以直接在main.go调用InitMission()

func main() {
  mission := InitMission("dj")

  mission.Start()
}

细心的童鞋可能发现了,wire.gowire_gen.go文件头部位置都有一个+build,不过一个后面是wireinject,另一个是!wireinject+build其实是 Go 语言的一个特性。类似 C/C++ 的条件编译,在执行go build时可传入一些选项,根据这个选项决定某些文件是否编译。wire工具只会处理有wireinject的文件,所以我们的wire.go文件要加上这个。生成的wire_gen.go是给我们来使用的,wire不需要处理,故有!wireinject

Compile-time Dependency Injection With Go Cloud's Wire - The Go Blog https://blog.golang.org/wire

 wire/guide.md at main · google/wire · GitHub https://github.com/google/wire/blob/main/docs/guide.md

Compile-time Dependency Injection With Go Cloud's Wire

Compile-time Dependency Injection With Go Cloud's Wire

Robert van Gent
9 October 2018

Overview

The Go team recently announced the open source project Go Cloud, with portable Cloud APIs and tools for open cloud development. This post goes into more detail about Wire, a dependency injection tool used in Go Cloud.

What problem does Wire solve?

Dependency injection is a standard technique for producing flexible and loosely coupled code, by explicitly providing components with all of the dependencies they need to work. In Go, this often takes the form of passing dependencies to constructors:

// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

This technique works great at small scale, but larger applications can have a complex graph of dependencies, resulting in a big block of initialization code that's order-dependent but otherwise not very interesting. It's often hard to break up this code cleanly, especially because some dependencies are used multiple times. Replacing one implementation of a service with another can be painful because it involves modifying the dependency graph by adding a whole new set of dependencies (and their dependencies...), and removing unused old ones. In practice, making changes to initialization code in applications with large dependency graphs is tedious and slow.

Dependency injection tools like Wire aim to simplify the management of initialization code. You describe your services and their dependencies, either as code or as configuration, then Wire processes the resulting graph to figure out ordering and how to pass each service what it needs. Make changes to an application's dependencies by changing a function signature or adding or removing an initializer, and then let Wire do the tedious work of generating initialization code for the entire dependency graph.

Why is this part of Go Cloud?

Go Cloud's goal is to make it easier to write portable Cloud applications by providing idiomatic Go APIs for useful Cloud services. For example, blob.Bucket provides a storage API with implementations for Amazon's S3 and Google Cloud Storage (GCS); applications written using blob.Bucket can swap implementations without changing their application logic. However, the initialization code is inherently provider-specific, and each provider has a different set of dependencies.

For example, constructing a GCS blob.Bucket requires a gcp.HTTPClient, which eventually requires google.Credentials, while constructing one for S3 requires an aws.Config, which eventually requires AWS credentials. Thus, updating an application to use a different blob.Bucket implementation involves exactly the kind of tedious update to the dependency graph that we described above. The driving use case for Wire is to make it easy to swap implementations of Go Cloud portable APIs, but it's also a general-purpose tool for dependency injection.

Hasn't this been done already?

There are a number of dependency injection frameworks out there. For Go, Uber's dig and Facebook's inject both use reflection to do runtime dependency injection. Wire was primarily inspired by Java's Dagger 2, and uses code generation rather than reflection or service locators.

We think this approach has several advantages:

  • Runtime dependency injection can be hard to follow and debug when the dependency graph gets complex. Using code generation means that the initialization code that's executed at runtime is regular, idiomatic Go code that's easy to understand and debug. Nothing is obfuscated by an intervening framework doing "magic". In particular, problems like forgetting a dependency become compile-time errors, not run-time errors.
  • Unlike service locators, there's no need to make up arbitrary names or keys to register services. Wire uses Go types to connect components with their dependencies.
  • It's easier to avoid dependency bloat. Wire's generated code will only import the dependencies you need, so your binary won't have unused imports. Runtime dependency injectors can't identify unused dependencies until runtime.
  • Wire's dependency graph is knowable statically, which provides opportunities for tooling and visualization.

How does it work?

Wire has two basic concepts: providers and injectors.

Providers are ordinary Go functions that "provide" values given their dependencies, which are described simply as parameters to the function. Here's some sample code that defines three providers:

// NewUserStore is the same function we saw above; it is a provider for UserStore,
// with dependencies on *Config and *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

// NewDefaultConfig is a provider for *Config, with no dependencies.
func NewDefaultConfig() *Config {...}

// NewDB is a provider for *mysql.DB based on some connection info.
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}

Providers that are commonly used together can be grouped into ProviderSets. For example, it's common to use a default *Config when creating a *UserStore, so we can group NewUserStore and NewDefaultConfig in a ProviderSet:

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)

Injectors are generated functions that call providers in dependency order. You write the injector's signature, including any needed inputs as arguments, and insert a call to wire.Build with the list of providers or provider sets that are needed to construct the end result:

func initUserStore() (*UserStore, error) {
    // We're going to get an error, because NewDB requires a *ConnectionInfo
    // and we didn't provide one.
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // These return values are ignored.
}

Now we run go generate to execute wire:

$ go generate
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed

Oops! We didn't include a ConnectionInfo or tell Wire how to build one. Wire helpfully tells us the line number and types involved. We can either add a provider for it to wire.Build, or add it as an argument:

func initUserStore(info ConnectionInfo) (*UserStore, error) {
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // These return values are ignored.
}

Now go generate will create a new file with the generated code:

// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject

func initUserStore(info ConnectionInfo) (*UserStore, error) {
    defaultConfig := NewDefaultConfig()
    db, err := NewDB(info)
    if err != nil {
        return nil, err
    }
    userStore, err := NewUserStore(defaultConfig, db)
    if err != nil {
        return nil, err
    }
    return userStore, nil
}

Any non-injector declarations are copied into the generated file. There is no dependency on Wire at runtime: all of the written code is just normal Go code.

As you can see, the output is very close to what a developer would write themselves. This was a trivial example with just three components, so writing the initializer by hand wouldn't be too painful, but Wire saves a lot of manual toil for components and applications with more complex dependency graphs.

How can I get involved and learn more?

The Wire README goes into more detail about how to use Wire and its more advanced features. There's also a tutorial that walks through using Wire in a simple application.

We appreciate any input you have about your experience with Wire! Wire's development is conducted on GitHub, so you can file an issue to tell us what could be better. For updates and discussion about the project, join the Go Cloud mailing list.

Thank you for taking the time to learn about Go Cloud's Wire. We’re excited to work with you to make Go the language of choice for developers building portable cloud applications.

 Go:一文读懂 Wire - Go语言中文网 - Golang中文社区 https://studygolang.com/articles/27163?

polaris · 2020-03-13 10:03:21 · 1739 次点击 · 预计阅读时间 11 分钟 · 3分钟之前 开始浏览    
这是一个创建于 2020-03-13 10:03:21 的文章,其中的信息可能已经有所发展或是发生改变。

本文作者:Che Dan

原文链接:https://medium.com/@dche423/master-wire-cn-d57de86caa1b

Wire 是啥

Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入。

依赖注入是保持软件 “低耦合、易维护” 的重要设计准则之一。

此准则被广泛应用在各种开发平台之中,有很多与之相关的优秀工具。

其中最著名的当属 Spring,Spring IOC 作为框架的核心功能对 Spring 的发展到今天统治地位起了决定性作用。

事实上, 软件开发 S.O.L.I.D 原则 中的“D”, 就专门指代这个话题。


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. 依赖关系静态存于源码之中, 便于工具分析与可视化

团队对 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


上手使用

安装很简单,运行go get github.com/google/wire/cmd/wire 之后, wire 命令行工具 将被安装到 $GOPATH/bin 。只要确保 $GOPATH/bin 在 $PATH中, wire 命令就可以在任何目录调用了。

在进一步介绍之前, 需要先解释 wire 中的两个核心概念: Provider 和 Injector:

Provider: 生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回。

组件可以是对象或函数 —— 事实上它可以是任何类型,但单一类型在整个依赖图中只能有单一 provider。因此返回 int 类型的 provider 不是个好主意。 对于这种情况, 可以通过定义类型别名来解决。例如先定义type Category int ,然后让 provider 返回 Category 类型

典型 provider 示例如下:

// 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){...}

实践中, 一组业务相关的 provider 时常被放在一起组织成 ProviderSet,以方便维护与切换。

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

Injector: 由wire自动生成的函数。函数内部会按根据依赖顺序调用相关 privoder 。

为了生成此函数, 我们在 wire.go (文件名非强制,但一般约定如此)文件中定义 injector 函数签名。 然后在函数体中调用wire.Build,并以所需 provider 作为参数(无须考虑顺序)。

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

// +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 函数的真正实现。 wire.go 中若有非 injector 的代码将被原样复制到 wire_gen.go 中(虽然技术上允许,但不推荐这样作)。 生成代码如下:

// 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 ,这个 build tag 确保在常规编译时忽略 wire.go 文件(因为常规编译时不会指定 wireinject 标签)。 与之相对的是 wire_gen.go 中的 //***+build*** !wireinject 。两组对立的 build tag 保证在任意情况下, wire.go 与 wire_gen.go 只有一个文件生效, 避免了“UserLoader 方法被重复定义”的编译错误
  2. 自动生成的 UserLoader 代码包含了 error 处理。 与我们手写代码几乎相同。 对于这样一个简单的初始化过程, 手写也不算麻烦。 但当组件数达到几十、上百甚至更多时, 自动生成的优势就体现出来了。

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

然后我们就可以使用真正的 injector 了, 例如:

package main

import "log"

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

如果不小心忘记了某个 provider, wire 会报出具体的错误, 帮忙开发者迅速定位问题。 例如我们修改 wire.go ,去掉其中的NewDb

// +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

将会报出明确的错误:“no provider found for *example.Db

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 中写入了未使用的 provider , 也会有明确的错误提示。


高级功能

谈过基本用法以后, 我们再看看高级功能

*接口注入*

有时需要自动注入一个接口, 这时有两个选择:

  1. 较直接的作法是在 provider 中生成具体类, 然后返回接口类型。 但这不符合Golang 代码规范。一般不采用
  2. 让 provider 返回具体类,但在 injector 声明环节作文章,将类绑定成接口,例如:
// 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
)

*属性自动注入*

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

// provider.gotype 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 可以简化此类工作, 指定属性名来注入特定属性:

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

如果要注入全部属性,则有更简化的写法:

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

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

type App struct {
    Foo *Foo
    Bar *Bar
    NoInject int `wire:"-"`
}

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

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

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

*值绑定*

虽不常见,但有时需要为基本类型的属性绑定具体值, 这时可以使用 wire.Value :

// provider.go
type Foo struct {
    X int
}// wire.go
...
wire.Build(wire.Value(Foo{X: 42}))
...

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

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

*把对象属性用作 Provider*

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

// provider
func provideBar(foo Foo)*Bar{
    return foo.Bar
}
// injector
...
wire.Build(provideFoo, provideBar)
...

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

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

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

*清理函数*

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

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

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

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

// +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

// 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() 。 意味着无论分配了多少资源, 只要调用过程不出错,他们的清理工作就会被集中到统一的清理函数中。 最终的清理工作由 injector 的调用者负责

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

然后就可以使用了:

func main() {
   app, cleanup, err := NewApp()
   if err != nil {
      log.Fatal(err)
   }
   defer cleanup()
   ...
}

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

 Go 每日一库之 wire - 知乎 https://zhuanlan.zhihu.com/p/110453784

之前的一篇文章Go 每日一库之 dig介绍了 uber 开源的依赖注入框架dig。读了这篇文章后,@overtalk推荐了 Google 开源的wire工具。所以就有了今天这篇文章,感谢推荐

 

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

 

快速使用

 

先安装工具:

 

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

 

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

 

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

 

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

 

type Monster struct {
  Name string
}

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

 

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

 

type Player struct {
  Name string
}

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

 

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

 

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!
", m.Player.Name, m.Monster.Name)
}

 

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

 

func main() {
  monster := NewMonster()
  player := NewPlayer("dj")
  mission := NewMission(player, monster)

  mission.Start()
}

 

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

 

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

 

//+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命令:

 

$ wire
wire: github.com/darjun/go-daily-lib/wire/get-started/after: 
wrote D:codegolangsrcgithub.comdarjungo-daily-libwireget-startedafterwire_gen.go

 

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

 

// 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中编写的代码一毛一样!接下来,我们可以直接在main.go调用InitMission()

 

func main() {
  mission := InitMission("dj")

  mission.Start()
}

 

细心的童鞋可能发现了,wire.gowire_gen.go文件头部位置都有一个+build,不过一个后面是wireinject,另一个是!wireinject+build其实是 Go 语言的一个特性。类似 C/C++ 的条件编译,在执行go build时可传入一些选项,根据这个选项决定某些文件是否编译。wire工具只会处理有wireinject的文件,所以我们的wire.go文件要加上这个。生成的wire_gen.go是给我们来使用的,wire不需要处理,故有!wireinject

 

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

 

注意,如果你运行时,出现了InitMission重定义,那么检查一下你的//+build wireinjectpackage main这两行之间是否有空行,这个空行必须要有!见。中招的默默在心里打个 1 好嘛

 

基础概念

 

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

 

参数

 

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

 

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

 

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

 

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

 

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

 

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{}
}

 

生成的代码如下:

 

func InitMission(m MonsterParam, p PlayerParam) Mission {
  player := NewPlayer(p)
  monster := NewMonster(m)
  mission := NewMission(player, monster)
  return mission
}

 

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

 

错误

 

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

 

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返回值:

 

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

 

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

 

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工具会报错!

 

高级特性

 

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

 

ProviderSet

 

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

 

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!
", 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!
", p.Player.Name, p.Monster.Name)
}

 

编写两个注入器:

 

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()都需要传入NewMonsterNewPlayer。两个还好,如果很多的话写起来就麻烦了,而且修改也不容易。这种情况下,我们可以先定义一个ProviderSet

 

var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

 

后续直接使用这个set

 

func InitEndingA(name string) EndingA {
  wire.Build(monsterPlayerSet, NewEndingA)
  return EndingA{}
}

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

 

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

 

结构构造器

 

因为我们的EndingAEndingB的字段只有PlayerMonster,我们就不需要显式为它们提供构造器,可以直接使用wire提供的结构构造器(Struct Provider)。结构构造器创建某个类型的结构,然后用参数或调用其它构造器填充它的字段。例如上面的例子,我们去掉NewEndingA()NewEndingB(),然后为它们提供结构构造器:

 

var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "Player", "Monster"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "Player", "Monster"))

func InitEndingA(name string) EndingA {
  wire.Build(endingASet)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(endingBSet)
  return EndingB{}
}

 

结构构造器使用wire.Struct注入,第一个参数固定为new(结构名),后面可接任意多个参数,表示需要为该结构的哪些字段注入值。上面我们需要注入PlayerMonster两个字段。或者我们也可以使用通配符*表示注入所有字段:

 

var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "*"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "*"))

 

wire为我们生成正确的代码,非常棒:

 

func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
  monster := NewMonster()
  endingA := EndingA{
    Player:  player,
    Monster: monster,
  }
  return endingA
}

 

 

绑定值

 

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

 

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{}
}

 

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

 

// wire_gen.go
func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
  monster := _wireMonsterValue
  endingA := NewEndingA(player, monster)
  return endingA
}

var (
  _wireMonsterValue = kitty
)

 

 

结构字段作为构造器

 

有时候我们编写一个构造器,只是简单的返回某个结构的一个字段,这时可以使用wire.FieldsOf简化操作。现在我们直接创建了Mission结构,如果想获得MonsterPlayer类型的对象,就可以对Mission使用wire.FieldsOf

 

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(结构名),后面跟多个参数表示将哪些字段作为构造器,*表示全部。

 

清理函数

 

构造器可以提供一个清理函数,如果后续的构造器返回失败,前面构造器返回的清理函数都会调用:

 

func NewPlayer(name string) (Player, func(), error) {
  cleanup := func() {
    fmt.Println("cleanup!")
  }
  if time.Now().Unix()%2 == 0 {
    return Player{}, cleanup, errors.New("player dead")
  }
  return Player{Name: name}, cleanup, nil
}

func main() {
  mission, cleanup, err := InitMission("dj")
  if err != nil {
    log.Fatal(err)
  }

  mission.Start()
  cleanup()
}

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

 

 

一些细节

 

首先,我们调用wire生成wire_gen.go之后,如果wire.go文件有修改,只需要执行go generate即可。go generate很方便,我之前一篇文章写过generate,感兴趣可以看看深入理解Go之generate

 

总结

 

wire是随着go-cloud的示例guestbook一起发布的,可以阅读guestbook看看它是怎么使用wire的。与dig不同,wire只是生成代码,不使用reflect库,性能方面是不用担心的。因为它生成的代码与你自己写的基本是一样的。如果生成的代码有性能问题,自己写大概率也会有 。

 

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue

 

参考

 

 

    1. wire GitHub:

[译]使用Go Cloud的Wire进行编译时依赖注入 https://juejin.cn/post/6844903689337962509

[译]使用Go Cloud的Wire进行编译时依赖注入

2018年10月9日

概述

Go团队最近公布了用于开放云开发的可移植云API和工具,开源项目Go Cloud 。 这篇文章详细介绍了Wire,一个随Go Cloud提供的依赖注入工具。

Wire解决了什么问题?

依赖注入是一种编写可伸缩、低耦合代码的标准技术。因为依赖注入显式地为组件提供他们需要工作的所有依赖关系。 在Go中,这通常采用将依赖项传递给构造函数的形式:

 // NewUserStore返回一个使用cfg和db作为依赖项的UserStore。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
复制代码

这种技术在小规模下工作得很好,但是较大的应用程序会存在一个复杂的依赖图。这导致了一大块依赖于顺序的初始化代码,这并不好玩。 因为一些依赖项被多次使用,通常很难干净地拆分这些代码。 将服务的一个实现替换为另一个实现也会很痛苦,因为这涉及到通过添加一组全新的依赖项(及其依赖项...)来修改依赖项图,并删除未使用的旧项。 实际上,在具有庞大依赖图的应用程序中更改初始化代码是繁琐且缓慢的。

像Wire这样的依赖注入工具旨在简化初始化代码的管理。您可以将您的服务及其依赖关系描述为代码或配置,然后Wire处理生成关系图,再据此确定初始化排序以及如何向每个服务传递它所需的依赖。 通过更改函数签名、添加或删除初始化程序来更改应用程序的依赖项,然后让Wire执行为整个依赖图生成初始化代码的繁琐工作。

为什么这是Go Cloud的一部分?

Go Cloud的目标是通过为合适的云服务提供惯用的Go API,使编写便携式云应用程序变得更加容易。 例如, blob.Bucket提供了一个存储API,其中包含亚马逊S3和谷歌云存储(GCS)的实现; 使用blob.Bucket编写的应用程序可以交换实现而无需更改其应用程序逻辑。 但是,初始化代码本质上是特定于提供者的,并且每个提供者具有不同的依赖集。

例如, 构建GCS blob.Bucket需要gcp.HTTPClient ,最终需要google.Credentials ,而为S3构建一个则需要aws.Config ,最终需要AWS凭据。 因此,更新应用程序以使用不同的blob.Bucket实现涉及到我们上面描述的依赖关系图的那种繁琐的更新。 Wire的驱动用例是为了方便交换Go Cloud可移植API的实现,但同时它也是依赖注入的通用工具。

这些工作不是已经做过了吗?

确实有许多依赖注入框架。对Go来说, Uber的digFacebook的inject都使用反射来进行运行时依赖注入。 Wire的主要灵感来自Java的Dagger 2 ,并且使用代码生成而不是反射或服务定位器 。

我们认为这种方法有几个优点:

  • 当依赖关系图变得复杂时,运行时依赖注入很难跟踪和调试。 使用代码生成意味着在运行时执行的初始化代码是常规的,惯用的Go代码,易于理解和调试。不会因为框架的各种奇技淫巧而变得生涩难懂。特别重要的是,忘记依赖项等问题会成为编译时错误,而不是运行时错误。
  • 服务定位器不同,不需要费心编造名称来注册服务。 Wire使用Go语法中的类型将组件与其依赖项连接起来。
  • 更容易防止依赖项变得臃肿。 Wire生成的代码只会导入您需要的依赖项,因此您的二进制文件将不会有未使用的导入。 运行时依赖性注入器在运行之前无法识别未使用的依赖项。
  • Wire的依赖图是静态可知的,这为工具化和可视化提供了可能。

它是如何工作的?

Wire有两个基本概念:提供者和注射器。

提供者是普通的Go函数,它们根据它们的依赖关系“提供”值,这些值被简单地描述为函数的参数。 以下是一些定义三个提供程序的示例代码:

 // NewUserStore与我们上面看到的功能相同; 它是UserStore的提供者,
 //依赖于*Config和*mysql.DB。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

 // NewDefaultConfig是*Config的提供者,没有依赖。
func NewDefaultConfig() *Config {...}

 // NewDB是基于某些连接信息的* mysql.DB的提供者。
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}
复制代码

通常一起使用的ProviderSets可以分组到ProviderSets 。 例如,在创建*UserStore时使用默认的*Config是很常见的,因此我们可以在ProviderSet中对NewUserStoreNewDefaultConfig进行分组:

var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
复制代码

注入器是被生成的函数,它们按依赖所需的顺序调用提供者。 编写注入器的签名,包括任何所需的输入作为参数,并插入对wire.Build的调用, wire.Build包含构造最终结果所需的提供者或提供者集的列表:

func initUserStore() (*UserStore, error) {
     //我们将得到一个错误,因为NewDB需要一个*ConnectionInfo
     //我们没有提供。
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // 这些返回值会被忽略。
}
复制代码

现在我们运行go generate来执行wire:

$ go generate
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed
复制代码

哎呀! 我们没有包含ConnectionInfo也没有告诉Wire如何构建一个。 Wire有用地告诉我们涉及的行号和类型。 我们可以将它的提供者添加到wire.Build ,或者将其添加为参数:

func initUserStore(info ConnectionInfo) (*UserStore, error) {
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // 这些返回值会被忽略。
}
复制代码

现在go generate将使用生成的代码创建一个新文件:

// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject

 func initUserStore(info ConnectionInfo)(* UserStore,error){
     defaultConfig:= NewDefaultConfig()
     db,err:= NewDB(info)
     if err!= nil {
        return nil, err
     }
     userStore,err:= NewUserStore(defaultConfig,db)
     if err!= nil {
        return nil, err
     }
     return userStore,nil
 }
复制代码

任何非注入器声明都将复制到生成的文件中。 在运行时没有依赖Wire:所有编写的代码都是正常的Go代码。

如您所见,输出非常接近开发人员自己编写的内容。 这只是一个简单的例子,只有三个组件,因此手工编写初始化程序并不会太痛苦,但Wire为具有更复杂依赖关系图的组件和应用程序节省了大量的手工操作。

我如何参与并了解更多信息?

Wire README详细介绍了如何使用Wire及其更高级的功能。 还有一个教程可以在一个简单的应用程序中使用Wire。

感谢您对Wire使用体验的任何意见! Go Cloud的开发是在GitHub上进行的,所以你可以提出一个问题来告诉我们什么可能更好。 有关项目的更新和讨论,请加入项目的邮件列表 。

感谢您抽出宝贵时间了解Go Cloud的Wire。 我们很高兴与您合作,使Go成为构建可移植云应用程序的开发人员的首选语言。

作者:Robert van Gent

原文:blog.golang.org/wire

原文地址:https://www.cnblogs.com/rsapaper/p/10145733.html