Wire - 基于依赖配置,自动生成Go代码初始化实现

AI 摘要: wire根据依赖配置文件自动生成初始化方法,解决Go代码中的依赖注入问题。使用wire可以避免手动编写大量的依赖注入代码,提高开发效率。本文介绍了wire的背景、核心概念、使用方法和参考资料。

1. wire 概述 - 基于依赖配置,自动生成 Go 代码初始化实现

wire 根据依赖配置文件,自动生成初始方法,解决 Go 代码中大量依赖注入代码初始问题

产生背景:

目前在实现较大型的 Go 应用或微服务开发过程中,采用了大量的依赖注入的技术:

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

通常较大的应用程序可能具有复杂的依赖关系图,比如 DDD 分层的代码在初始化会有大量的初始化工作( interface接口实现 -> application应用实现 -> service实现 -> 仓储接口 <- 基础设施实现 ,参考最后 Example Demo 示例),我们需要依次实例化各类结构体的实例(通过参数 New 生成各类结构体实例,将生成的参数再依次注入到其他方法,然后通过手动处理各个实体直接的依赖关系,依次调用实现),这类情况我们可以wire 很好的解决这类繁琐的初始过程,从而提高生产力。

Wire 等依赖注入工具目标:

旨在简化初始化代码的管理,以代码或配置的形式描述服务及其依赖关系。

然后 Wire 处理生成的图形以确定排序以及如何传递每个服务所需的内容。通过更改函数签名或添加或删除初始化程序来更改应用程序的依赖项,然后让 Wire 完成为整个依赖关系图生成初始化代码的繁琐工作。

2. wire 命令行安装

wire 安装 :go install github.com/google/wire/cmd/wire@latest

这个命令行工具主要是用于根据配置的wire.go生成wire_gen.go文件

3. wire 两个核心概念 - providers(提供者)和 injectors(注入器)

3.1. provider - 提供者

  1. 定义 Provider,一个方法可以生成一个值,方法支持多参数传递、支持错误返回
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
    return Foo{X: 42}
}

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

// 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
}
  1. 一批常用的 provider 支持通过wire.NewSet分组进行provider sets 组合,方便复用
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package foobarbaz

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

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

package foobarbaz

import (
    // ...
    "example.com/some/other/pkg"
)

// ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

3.2. injector - 注入器

一个应用通过一个injector穿起这些providers,injector 实际就是一个函数按依赖的顺序调用 provider;依据注入器(injectors)的签名,通过使用 Wire 生成工具,就可以生成函数的 Body。

一个注入器申明在一个wire.Build() 的函数体内,返回值不影响正确类型

下面代码生成一个获取Baz 的注入器,同providers一样,注入器injectors也可以支持入参(用于发送给providers)和返回错误。

 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
}

**生成:**可以通过在包目录中,调用 wire 来生成注入器,生成文件为wire_gen.go , 一旦该文件创建后,可以重新通过执行 go generate 执行

注意:在带有注入器的文件中找到的任何非注入器声明都将复制到生成的文件中

 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 go run -mod=mod github.com/google/wire/cmd/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 foobarbaz.Baz{}, err
    }
    return baz, nil
}

4. wire 最佳实践

参考:https://github.com/google/wire/blob/main/docs/best-practices.md

  1. 最佳实践:type MySQLConnectionString string, 如果您需要注入一个通用类型,例如string,请创建一个新的字符串类型以避免与其他提供程序发生冲突

  2. 选项参数结构,包含许多依赖项的提供程序函数可以将函数选项结构配对

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    type Options struct {
        // Messages is the set of recommended greetings.
        Messages []Message
        // Writer is the location to send greetings. nil goes to stdout.
        Writer io.Writer
    }
    
    func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
        // ...
    }
    
    var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)
    
  3. 通常,更喜欢小型提供者集,避免较大的提供者集能够降低应用程序遇到冲突的可能性。当使用 ProviderSet 将一组相关的 Provider 进行关联时候,后续维护的时候要注意可能导致 wire 执行失败的问题,比如新增了 provider 依赖、移除了 provider、错误的 provider 类型(指针和非指针类型)、provider 冲突等

  4. 最佳实践: 不要在同一个基础设施初始化加入多个不同的 dsn(通用类型无法区分),必要时候可以通过抽出一个选项结构体DBConfig,以及对应的方法(如InitBussDBConfig() *DBConfig )初始化,进一步方便 wire 做初始化

5. wire FAQ

  1. inject (INJECTOR) : no provider found for (PROVIDER_A)... needed by ... 通常主要原因是注入器的类型错误,比如期望参数是结构体指针,但传入的非指针类型

  2. 接口实现如何绑定? — wire.Bind()

  3. 注入器的返回类型错误? 通常是因为provider 缺少或者provider 依赖的参数类型错误导致provider无法实现

  4. 避免生成的 injector 注入器方法在wire.gowire_gen.go 冲突,可以在wire.go中包头加入flag标识为仅作为构建使用

    1
    2
    3
    4
    5
    
    //go:build wireinject
    // +build wireinject
    
    // The build tag makes sure the stub is not built in the final build.
    package xxx
    
  5. 最佳实践: 不要在同一个基础设施初始化加入多个不同的 dsn,有必要可以通过抽出一个 Options 或者 Config,以及对应的方法进一步方便 wire 做初始化

  6. wire 是如何实现的? Wire 主要受到 Java 的Dagger 2的启发,并使用代码生成而不是反射或服务定位器。这样做不会引入任何“魔法”干预,在运行时执行的初始化代码是常规的、惯用的 Go 代码,易于理解和调试。 — 运行时依赖注入可能很难跟踪和调试; 同时,Wire 使用 Go 类型将组件与其依赖项连接起来,基于类型比较简单;

  7. 注意:任何非注入器声明都被复制到生成的文件中

6. wire Example 示例

6.1. 示例 - DDD 微服务应用初始化

  1. twire.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package twire

import (
    "context"
    "log"
)

// App 应用
type App struct {
    srv   *Service
    repos IRepos
}

// NewApp 创建应用实例
func NewApp(srv *Service, repos IRepos) (*App, error) {
    return &App{
        srv:   srv,
        repos: repos,
    }, nil
}

func (app *App) DoThings() error {
    return app.srv.repos.ExecSomething(context.Background(), 88)
}

// IRepos 仓储接口
type IRepos interface {
    ExecSomething(ctx context.Context, id uint32) error
}

// BussInfra 实现
type BussInfra struct {
    dsn string
}

func (b *BussInfra) ExecSomething(ctx context.Context, id uint32) error {
    log.Printf("infra dsn [%s], id#%v exec something...", b.dsn, id)
    return nil
}

// NewBussInfra 创建基础设施实例
// 尽量不要在同一个基础设施初始时候,加入多个不同的dsn,可以通过抽出一个Options或者Config,以及对应的方法进一步方便wire做初始化
func NewBussInfra(dsn string) (*BussInfra, error) {
    return &BussInfra{dsn: dsn}, nil
}

// Service 服务
type Service struct {
    repos IRepos
}

// NewService 创建服务
func NewService(repos IRepos) *Service {
    return &Service{repos: repos}
}
  1. 配置wire.go 配置创建一个*App应用实例的注射器injector
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//go:build wireinject
// +build wireinject

// The build tag makes sure the stub is not built in the final build.
package twire

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

func InitApp(dsn string) (*App, error) {
    wire.Build(
        NewBussInfra,
        NewApp,
        NewService,
        wire.Bind(new(IRepos), new(*BussInfra)),
    )
    return &App{}, nil
}
  1. 命令行执行 wire 命令
1
2
3
/private/data/x-learn/go-refs/third/twire/twire on  master! (c74860a) 💰 10:32:27
$ wire
wire: x-learn/third/twire/twire: wrote /private/data/x-learn/go-refs/third/twire/twire/wire_gen.go
  1. 生成的 wire.go 文件,和之前手动生成很雷同了
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package twire

// Injectors from wire.go:

func InitApp(dsn string) (*App, error) {
    bussInfra, err := NewBussInfra(dsn)
    if err != nil {
        return nil, err
    }
    service := NewService(bussInfra)
    app, err := NewApp(service, bussInfra)
    if err != nil {
        return nil, err
    }
    return app, nil
}
  1. main.go 执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "x-learn/advance/klog/log"
    "x-learn/third/twire/twire"
)

func main() {
    app, err := twire.InitApp("User:Pass")
    if err != nil {
        log.Fatal(err)
    }
    err = app.DoThings()
    if err != nil {
        log.Fatal(err)
    }
}

// 输出 2023/02/03 11:42:37 infra dsn [User:Pass], id#88 exec something...

6.2. 示例 - Struct 的初始参数处理

包含许多依赖项的提供程序函数可以将函数与选项结构配对

  1. xstruct.go 文件 - 这里 NewGreeter 初始化依赖一个*Options 指针参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xstruct

import (
    "context"
    "io"
)

type Greeter struct {
}

type Options struct {
    // Messages is the set of recommended greetings.
    Messages []string
    // Writer is the location to send greetings. nil goes to stdout.
    Writer io.Writer
}

func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
    // ...

    return nil, nil
}
  1. **wire.go 文件 ** - 定义一个 initXStructGreet(msgs []string, writer io.Writer) (Greeter, error) 方法,生成 Greeter 实例,这里的initXStructGreet()返回注意要和NewGreeter()对齐
 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
//go:build wireinject
// +build wireinject

package xstruct

import (
    "context"
    "io"

    "github.com/google/wire"
)

// 注入器集合
var GreeterSet = wire.NewSet(
    context.Background,
    wire.Struct(new(Options), "*"),
    NewGreeter,
)

// 新的注入器实现
func InitXStructGreet(msgs []string, writer io.Writer) (*Greeter, error) {
    wire.Build(
        GreeterSet,
    )

    return &Greeter{}, nil
}
  1. 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
29
30
31
32
33
34
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package xstruct

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

// Injectors from wire.go:

// 新的注入器实现
func InitXStructGreet(msgs []string, writer io.Writer) (*Greeter, error) {
    contextContext := context.Background()
    options := &Options{
        Messages: msgs,
        Writer:   writer,
    }
    greeter, err := NewGreeter(contextContext, options)
    if err != nil {
        return nil, err
    }
    return greeter, nil
}

// wire.go:

// 注入器集合
var GreeterSet = wire.NewSet(context.Background, wire.Struct(new(Options), "*"), NewGreeter)

7. 小结

wire 是在依赖注入的代码风格,加上大型项目初始手动操作繁琐的背景下产生的。

wire 有两个核心概念,一个是provider提供者,通常会有多个;另一个是 injector注入器,用于生成相关初始实例。provider在接口实现时候,需要通过wire.Bind() 实现,这里特别注意 provider 提供返回的类型,和注入的参数类型是一致,否则会提示no provider 错误injector注入器包含了相关依赖配置,通过wire.Build()申明,为了复用和管理,可以通过wire.NewSet()申明一组相关的 Provider 集合,不过使用集合时候需要注意冲突问题;

wire.go配置依赖关系时候,可以通过// +build wireinjectflag 配置在包头,将wire.go配置文件忽略,避免重名问题。

wire 在刚开始直接上手使用时候,会有一些难理解,可以多手动 wire 生成几个 demo 后,就比较容易理解了

8. 参考