介绍

参考 依赖注入 一文

调研范围

本文将调研如下几个比较主流的 Go 依赖注入库和框架。

仓库stars(截止 2021-10-03)
google/wire6.6k
uber-go/dig2.1K
uber-go/fx2.3k
go-spring/go-spring930

实验项目

创建

创建一个实现项目 go-dependency-injection-learn 对以上库进行体验(go 1.17)

mkdir go-dependency-injection-learn
cd go-dependency-injection-learn
go mod tidy
go mod init github.com/rectcircle/go-dependency-injection-learn

简单场景 Bean

Bean 结构如下:

  • A 包含一个 string 类型字段
  • B 包含一个 int 类型字段
  • C 包含 A 和 B

现在需要将 A 和 B 构造出来并注入到 B 中。

代码如下,创建包目录 mkdir -p bean/sample,并创建文件 bean/sample/sample.go

package sample

import "fmt"

// 类型 A 和 构造器

type A struct {
	aField string
}

func NewA(aField string) *A {
	return &A{
		aField: aField,
	}
}

// 类型 B 和 构造器

type B struct {
	bField int
}

func NewB(bField int) *B {
	return &B{
		bField: bField,
	}
}

// 类型 B 和 构造器

type C struct {
	a *A
	b *B
}

func (c *C) String() string {
	return fmt.Sprintf("I am C and c.a is %s, c.b is %d", c.a.aField, c.b.bField)
}

func NewC(a *A, b *B) *C {
	return &C{
		a: a,
		b: b,
	}
}

// 假设最终要构造一个 C,写法如下

func ManualInitialize(aField string, bField int) *C {
	a := NewA(aField)
	b := NewB(bField)
	c := NewC(a, b)
	return c
}

仓库地址

https://github.com/rectcircle/go-dependency-injection-learn

wire

安装并添加依赖

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

创建包目录 mkdir wire

简单场景例子

编写代码

编写声明文件 wire/wire.go

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

package main

import (
	"github.com/google/wire"
	"github.com/rectcircle/go-dependency-injection-learn/bean/sample"
)

// 初始化声明
func InitializeSample(aField string, bField int) *sample.C {
	panic(wire.Build(sample.NewA, sample.NewB, sample.NewC))
}

编写调用者函数 wire/main.go

package main

import "fmt"

func main() {
	fmt.Printf("call InitializeSample: %s\n", InitializeSample("test", 1))
}

执行代码生成

# 代码生成(使用 wireinject tag 让编译器去扫描该代码文件)
go generate --tags wireinject -v ./... # 或者 wire ./wire 

将生成一个 wire/wire_gen.go 文件,文件内容为

// Code generated by Wire. DO NOT EDIT.

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

package main

import (
	"github.com/rectcircle/go-dependency-injection-learn/bean/sample"
)

// Injectors from wire.go:

// 依赖注入声明
func InitializeSample(aField string, bField int) *sample.C {
	a := sample.NewA(aField)
	b := sample.NewB(bField)
	c := sample.NewC(a, b)
	return c
}

wire/wire.go 说明

  • //go:generate wire 第一行声明一个生成器。这样,开发者只需在项目根目录执行 go generate --tags wireinject -v ./... 即可快速更新声明代码。
  • //go:build wireinject 第二行为 Go 1.17 及以上版本新的条件编译注释,表示只有包含 --tags wireinject 时该文件才参与编译。条件编译是 wire 利用的核心能力,在进行代码生成的时候,go 编译器会解析 wire/wire.go 文件,生成一个 wire/wire_gen.go。在编译阶段,编译器将只认识 wire/wire_gen.go,而忽略 wire/wire.go,从而将生成的代码编译到产物中。
  • // +build wireinject 第三行为 Go1.16 即之前版本的条件编译声明,目的和与第二个相同
  • 函数(Injector),wire 会扫描该文件中的所有函数,并根据函数声明类型,返回值类型,以及 wire.Build 传递的构造函数(Provider),构建一个依赖关系树,并将该函数的函数体,生成到 wire/wire_gen.go 文件中。

wire/wire_gen.go 说明

  • 第一行为,告诉 IDE 该文件是由程序自动生成的,以禁止用户修改
  • //go:generate 为声明代码生成器,以支持 go generate 命令可以更新生成的代码
  • //+build !wireinject 为条件编译,表示不包含 wireinject 标签时,识别该代码文件
  • 函数 (Injector),根据 wire/wire 中函数生成的

核心概念

官方博客

Provider

Provider 一个普通的 Go 函数,可以认为是一个构造函数(不过还存在其他形式),下面有几个例子

func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

func NewDefaultConfig() *Config {...}

func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}

一组有树状调用关系的 Providers 可以组成 ProviderSet,并将会作为 wire.Build 的参数列表之一,wire 在生成代码的时候,会分析这些 Provider 的树状调用关系,并生成代码。

如想使用 wire,则需要先声明一系列 Provider

Injector

Injector 是按依赖顺序调用 Provider 的生成函数。开发者只需要编写 Injector 签名,包括任何需要的输入作为参数,并插入对 wire.Build 的调用,其参数为 provider 列表或 ProviderSet 表,例子如下:

func initUserStore() (*UserStore, error) {
    wire.Build(UserStoreSet, NewDB)
    return nil, nil
}

原理

编译时,构建一颗调用链树,数的根节点是 Injector 的返回值,叶子节点是 Injector 的参数和无参Provider,非叶子节点是有参 Provider。

另外 wire 构建树的边是根据类型而非名称进行匹配的,因此就要保证:

  • 同一个 Provider 不能有相同的类型的参数
  • Injector 的参数不能有相同类型的参数

最终 wire 将这棵树转换为 go 代码写入 *_gen.go 文件。

VSCode 配置

由于 wire 依赖 go 条件编译,因此如果想要对 Injector 声明文件添加智能提示,需要添加如下配置

.vscode/settings.json

{
	"gopls": {
		"buildFlags": ["-tags=wireinject"]
	},
}

另附上 VSCode 调试配置

.vscode/tasks.json

{
	// See https://go.microsoft.com/fwlink/?LinkId=733558
	// for the documentation about the tasks.json format
	"version": "2.0.0",
	"tasks": [
		{
			"label": "mirePreLaunchTask",
			"type": "shell",
			"command": "go generate --tags wireinject -v ./...",
		}
	]
}

.vscode/launch.json

{
	// 使用 IntelliSense 了解相关属性。 
	// 悬停以查看现有属性的描述。
	// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
	"version": "0.2.0",
	"configurations": [
		{
			"name": "Launch wire",
			"type": "go",
			"request": "launch",
			"preLaunchTask": "mirePreLaunchTask",
			"mode": "auto",
			"program": "${workspaceFolder}/wire"
		}
	]
}

特性

使用流程

  • 声明 / 定义 Provider
  • 声明 Injector
  • 调用 wire 命令生成代码
  • 编译运行

Provider Set

当一些provider通常是一起使用的时候,可以使用 provider set 将它们组织起来。

wire/wire.go

//不能声明在 Injector 里面
var sampleSet = wire.NewSet(sample.NewA, sample.NewB, sample.NewC)

func InitializeSample2(aField string, bField int, c bool /*无用输入不会报错*/) *sample.C {
	panic(wire.Build(sampleSet))
}

wire/main.go

	fmt.Printf("call InitializeSample2: %s\n", InitializeSample2("test2", 2, true))

接口绑定

将接口类型和结构体类型绑定,使得调用树可以搭建

wire/interface_bind.go

package main

import "github.com/google/wire"

// Fooer - 接口
type Fooer interface {
	Foo() string
}

// MyFooer - 接口 Fooer 的实现
type MyFooer string

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

func newMyFooer() *MyFooer {
	foo := MyFooer("Hello, World!")
	return &foo
}

// 结构体 Bar

type Bar string

func newBar(f Fooer) string {
	return f.Foo()
}

var InterfaceSet = wire.NewSet(
	newMyFooer,
	wire.Bind(new(Fooer), new(*MyFooer)), // 将结构体类型和接口绑定
	newBar)

wire/wire.go

// 3. 结构体绑定
func InitializeWithInterfaceBind() string {
	panic(wire.Build(InterfaceSet))
}

wire/main.go

	fmt.Printf("call InitializeWithInterfaceBind: %s\n", InitializeWithInterfaceBind())

属性注入 Provider

wire/attribute_injection_provider.go

package main

import "github.com/google/wire"

type Foo2 int
type Bar2 int

func newFoo2() Foo2 { return 1 }

func newBar2() Bar2 { return 2 }

type FooBar2 struct {
	MyFoo2   Foo2
	MyBar2   Bar2
	MyBar2_2 Bar2 `wire:"-"` // 忽略该字段
}

var StructProviderSet = wire.NewSet(
	newFoo2,
	newBar2,
	wire.Struct(new(FooBar2), "MyFoo2")) // 只绑定一个参数

var StructProviderSet2 = wire.NewSet(
	newFoo2,
	newBar2,
	wire.Struct(new(FooBar2), "*")) // * 表示注入全部字段`wire:"-"` 的除外

wire/wire.go

// 4. 结构体 Provider
func InitializeStructProvider() FooBar2 { // 返回结构体
	panic(wire.Build(StructProviderSet))
}
func InitializeStructProvider2() *FooBar2 { // 返回结构体指针
	panic(wire.Build(StructProviderSet2))
}

wire/main.go

	fmt.Printf("call InitializeStructProvider: %#v\n", InitializeStructProvider())
	fmt.Printf("call InitializeStructProvider2: %#v\n", InitializeStructProvider2())

值 Provider

wire/value_privider.go

package main

import (
	"io"
	"os"

	"github.com/google/wire"
)

type Foo3 struct {
	X int
}

var BindValueSet1 = wire.NewSet(wire.Value(Foo3{X: 42}))
var BindValueSet2 = wire.NewSet(wire.InterfaceValue(new(io.Reader), os.Stdin))

wire/wire.go

// 5. 绑定值
func InitializeValue1() Foo3 {
	panic(wire.Build(BindValueSet1))
}
func InitializeValue2() io.Reader {
	panic(wire.Build(BindValueSet2))
}

wire/main.go

	fmt.Printf("call InitializeValue1: %#v\n", InitializeValue1())
	fmt.Printf("call InitializeValue2: %#v\n", InitializeValue2())

结构体字段 Provider

wire/struct_field_provider.go

package main

import "github.com/google/wire"

type Foo4 struct {
	S string
	N int
	F float64
}

func NewFoo4() Foo4 {
	return Foo4{S: "Hello, World!", N: 1, F: 3.14}
}

var StructFieldProviderSet = wire.NewSet(
	NewFoo4,
	wire.FieldsOf(new(Foo4), "S"))

wire/wire.go

// 6. 结构体字段 Provider
func InitializeStructField() string {
	panic(wire.Build(StructFieldProviderSet))
}

wire/main.go

	fmt.Printf("call InitializeStructField: %s\n", InitializeStructField())

返回错误

wire/error.go

package main

import "errors"

func newStringOrError(isErr bool) (string, error) {
	if isErr {
		return "", errors.New("模拟构造函数执行抛出异常")
	}
	return "hello World", nil
}

wire/wire.go

// 7. 返回错误
func InitializeTestError(isErr bool) (string, error) {
	panic(wire.Build(newStringOrError))
}

wire/main.go

	s, e := InitializeTestError(true)
	fmt.Printf("call InitializeTestError(true): %s, %v\n", s, e)
	s, e = InitializeTestError(false)
	fmt.Printf("call InitializeTestError(false): %s, %v\n", s, e)

清理函数

该清理函数指的是构建成功的清理函数。

wire/cleanup.go

package main

import (
	"errors"
	"fmt"
)

func newStringOrCleanup(isErr bool) (string, func(), error) {
	if isErr {
		return "", nil, errors.New("模拟构造函数执行抛出异常")
	}
	return "hello World", func() {
		fmt.Println("调用了 cleanup 函数")
	}, nil
}

wire/wire.go

// 8. Cleanup
func InitializeTestCleanup(isErr bool) (string, func(), error) {
	panic(wire.Build(newStringOrCleanup))
}

wire/main.go

	s, cleanup, e := InitializeTestCleanup(true)
	fmt.Printf("call InitializeTestCleanup(true): %s, %v\n", s, e)
	if cleanup != nil {
		cleanup()
	}
	s, cleanup, e = InitializeTestCleanup(false)
	fmt.Printf("call InitializeTestCleanup(false): %s, %v\n", s, e)
	if cleanup != nil {
		cleanup()
	}

最佳实践

掘金 | 官方

区分类型

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

type FooBar struct {
	foo string
	bar string
}

func NewFooBar(foo string, bar string) FooBar {
	return FooBar{
	    foo: foo,  
	    bar: bar,
	}
}

func InitializeFooBar(a string, b string) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}

生成代码,将报错:provider has multiple parameters of type string

因此需要,对 String 进行类型定义

type Foo string
type Bar string
type FooBar struct {
	foo Foo
	bar Bar
}
// ...

Option Struct

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

type Message string

// Options
type Options struct {
	Messages []Message
	Writer   io.Writer
	Reader   io.Reader
}
type Greeter struct {
}

// NewGreeter Greeter的provider方法使用Options以避免构造函数过长
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
	return nil, nil
}
// GreeterSet 使用wire.Struct设置Options为provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)

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

Provider Sets 在 Libraries 中声明的注意事项

Provider Sets 声明到库里面时。在迭代过程中,不应该破坏Provider Set的兼容性。

如下更改不会破坏兼容性

  • 删除Provider无用的输入的声明(修改某个 Provider)
  • 添加新的输出,且该类型是新建的不会和用户附加的 Provider 产生冲突

Mock

参见:官方

缺点

dig

添加依赖

go get 'go.uber.org/dig@v1'

简单场景例子

创建包目录 mkdir dig,并编写代码 dig/sample.go

package main

import (
	"fmt"
	"log"

	"github.com/rectcircle/go-dependency-injection-learn/bean/sample"
	"go.uber.org/dig"
)

func RunSample(a string, b int) {
	c := dig.New()
	// 注册构造函数
	errs := []error{
		// 注册构造函数需要的参数
		c.Provide(func() (string, int) { return a, b }),
		// 注册构造函数
		c.Provide(sample.NewA),
		c.Provide(sample.NewB),
		c.Provide(sample.NewC),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			log.Fatal(err)
		}
	}
	// 调用函数,并将 bean 注入函数参数
	err := c.Invoke(func(c *sample.C) {
		fmt.Printf("RunSample: %s\n", c)
	})
	if err != nil {
		fmt.Println(c)
		log.Fatalln(err)
	}
	fmt.Println(c)
    fmt.Println("print dot graph")
	dig.Visualize(c, os.Stdout)
	fmt.Println()
}

dig/main.go

package main

func main() {
	RunSample("string", 1)
}

go run ./dig 输出

nodes: {
	 *sample.B -> deps: [int], ctor: func(int) *sample.B
	 *sample.C -> deps: [*sample.A *sample.B], ctor: func(*sample.A, *sample.B) *sample.C
	 string -> deps: [], ctor: func() (string, int)
	 int -> deps: [], ctor: func() (string, int)
	 *sample.A -> deps: [string], ctor: func(string) *sample.A
}
values: {
	 *sample.C => I am C and c.a is string, c.b is 1
	 string => string
	 int => 1
	 *sample.A => &{string}
	 *sample.B => &{1}
}

核心 API

  • dig.New 创建一个依赖注入容器
  • func (*dig.Container).Provide(constructor interface{}, opts ...dig.ProvideOption) error 注册一个构造函数到容器里面。该动作不会触发依赖图完整性检查,也不会执行该函数,仅仅执行注册并构建一个依赖图。如下情况下才会返回 error
    • constructor 为 nil,或者不是一个函数
    • constructor 没有返回大于等于一个非 error 返回值
    • constructor 返回值的类型已经存在,且没有命名
    • opts 是非法的
  • func (*dig.Container).Invoke(function interface{}, opts ...dig.InvokeOption) error 解析 function,对照其参数类型,去容器里面去找或者构建相关对象。另外只有调用 Invoke,Provider 注册的构造函数才会被执行,且最多执行一次。也就是说 dig 采用单例模式。如下情况下才会返回 error:
    • function 为 nil,或者不是一个函数
    • function 的参数依赖图不完整,无法构建
    • function 的依赖路径中存在环(循环依赖)
    • function 最后一个返回参数是 error,将原样返回
  • func (*dig.Container).String() string 返回已经注册构造函数和依赖关系,以及已经构造出来的对象,可用于 debug。
  • dig.Visualize(c, os.Stdout) 打印依赖图的 dot graph,可用于测试和观察层次关系

原理

运行时,利用反射分析 构造函数 关系,并构建对象。

VSCode 配置

.vscode/launch.json 的 configurations 数组添加如下

		{
			"name": "Launch dig",
			"type": "go",
			"request": "launch",
			"mode": "auto",
			"program": "${workspaceFolder}/dig"
		},

特性

接口绑定 - as

说明

  • 如果构造函数返回多个值,dig.As 将报错 v
  • dig.As 可以和 dig.Name 一起使用 v
  • dig.As 不可以和 dig.Group 一起使用
  • dig.As 的参数只允许是接口指针类型
  • 如果构造函数返回的值实现了多个接口,则 dig.As 可以指定多个

dig/as.go

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"

	"go.uber.org/dig"
)

// Fooer - 接口
type Fooer interface {
	Foo() string
}

// MyFooer - 接口 Fooer 的实现
type MyFooer string

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

func newMyFooer() *MyFooer {
	foo := MyFooer("Hello, World!")
	return &foo
}

// 结构体 Bar

type Bar string

func newBar(f Fooer) string {
	return f.Foo()
}

func RunWithInterfaceError1() {
	c := dig.New()

	err := c.Provide(func() (*bytes.Buffer, *bytes.Buffer) {
		return nil, nil
	}, dig.As(new(io.Reader), new(io.Writer)))
	// 错误处理
	if err != nil {
		fmt.Println(c)
		fmt.Printf("错误场景 1 - 构造函数不支持返回多个值: %s\n", err)
	}
}

func RunWithMultipleInterfaceAndName() {
	c := dig.New()

	err := c.Provide(func() *bytes.Buffer {
		return nil
	}, dig.As(new(io.Reader), new(io.Writer)), dig.Name("buffer")) // 通过 as 将结构体和接口进行绑定
	// 错误处理
	if err != nil {
		fmt.Println(c)
		log.Fatal(err)
	}
	fmt.Printf("RunWithMultipleInterfaceAndName: %s", c.String())
}

func RunWithInterfaceMultiple() {
	c := dig.New()

	err := c.Provide(func() (*bytes.Buffer, *bytes.Buffer) {
		return nil, nil
	}, dig.As(new(io.Reader), new(io.Writer))) // 通过 as 将结构体和接口进行绑定
	// 错误处理
	if err != nil {
		fmt.Println(c)
		log.Fatal(err)
	}
	fmt.Printf("返回多个不同接口的值 RunWithInterfaceMultiple: %s", c.String())
}

func RunWithInterfaceError2() {
	c := dig.New()
	// 注册构造函数
	err := c.Provide(newMyFooer, dig.As(new(Fooer)), dig.Group("test")) // 通过 as 将结构体和接口进行绑定
	// 错误处理
	if err != nil {
		fmt.Println(c)
		fmt.Printf("错误场景 2 - `dig.As` 不可以和 `dig.Group` 一起使用: %s\n", err)
	}
}

func RunWithInterfaceError3() {
	c := dig.New()
	// 注册构造函数
	err := c.Provide(newMyFooer, dig.As(new(MyFooer))) // 通过 as 将结构体和接口进行绑定
	// 错误处理
	if err != nil {
		fmt.Println(c)
		fmt.Printf("错误场景 3 - `dig.As` 的参数只允许是接口指针类型: %s\n", err)
	}
}

func RunWithInterface() {

	RunWithInterfaceError1()
	RunWithMultipleInterfaceAndName()
	RunWithInterfaceError2()
	RunWithInterfaceError3()

	c := dig.New()
	// 注册构造函数
	errs := []error{
		c.Provide(newMyFooer, dig.As(new(Fooer))), // 通过 as 将结构体和接口进行绑定
		c.Provide(newBar),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			log.Fatal(err)
		}
	}
	// 调用函数,并将 bean 注入函数参数
	err := c.Invoke(func(result string) {
		fmt.Printf("RunWithInterface: %s\n", result)
	})
	if err != nil {
		fmt.Println(c)
		log.Fatalln(err)
	}
	fmt.Println(c.String())
}

dig/main.go

	RunWithInterface()

参数对象、结果对象和可选依赖

  • 参数对象,用在 Provide 和 Invoke 的函数参数的参数上,用来简化代码,参数对象为包含
  • 结果对象,当 Provide 传入的 constructor 返回结果过多时,可以考虑使用结果对象,以简化代码
  • 注意:参数对象和结果对象不允许包含私有(未导出)字段(就算添加 optional 也不行)

dig/parameter_result_objects_and_optional.go

package main

import (
	"fmt"
	"log"

	"go.uber.org/dig"
)

type ABEIn struct {
	dig.In // 参数化对象,需嵌入该结构体作为表示,可以用在 Provide 和 Invoke 第一个参数的参数中
	A      int
	B      string
	E      int16 `optional:"true"` // 可选依赖,如果不存在,则为 0 值或者 nil
	c      bool  `optional:"true"` // dig.In 不允许有私有(未导出)字段,optional 也不行
}

type BCOut struct {
	dig.Out // 结果对象,需嵌入该结构体作为表示,可以用在 Provide 的第一个参数的返回值中使用
	B       string
	C       bool
}

func newBC() BCOut {
	return BCOut{
		B: "b",
		C: true,
	}
}

func newA() int {
	return 1
}

func newD(_ ABEIn) int8 {
	return 2
}

func RunParameterResultObjects() {
	c := dig.New()
	// 注册构造函数
	errs := []error{
		// 调用 newBC 的返回值 BCOut,dig 会将 BCOut 的每个字段作为 value 作为放到容器中,而不是将 BCOut 放入容器中
		c.Provide(newBC),
		c.Provide(newA),
		// dig 会从容器中查询 ABIn 的每个字段,并构建 ABIn 结构体,然后再调用该函数
		c.Provide(newD),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			log.Fatal(err)
		}
	}
	// 调用函数,并将 bean 注入函数参数
	err := c.Invoke(func(abe ABEIn, c bool, d int8) {
		// dig 会从容器中查询 ABIn 的每个字段,并构建 ABIn 结构体,然后再调用该函数
		fmt.Printf("RunParameterResultObjects: a=%d, b=%s, c=%t, d=%d, e=%d\n", abe.A, abe.B, c, d, abe.E)
	})
	if err != nil {
		fmt.Println(c)
		log.Fatalln(err)
	}
	fmt.Println(c)
}

dig/main.go

	RunParameterResultObjects()

命名值和组

  • 命名值,注册的构造函数的返回值存在相同类型时,dig 不知到该注入哪一个。因此 dig 提供命名值的机制来对同类型的值进行命名。使用命名值的注意事项如下:
    • 不支持在同一个函数里返回同一类型的多个值,比如 func newInt0And1() (int, int)
    • 命名值不能注入到非命令名参数中,比如 c.Provide(newInt2, dig.Name("int2")) 然后 c.Invoke(func(int_ int){}) 将报错
    • 不允许同一类型存在多个相同的命名
    • 针对同一类型,允许存在 0 个或 1 个未命名值,多个命名不同的命名值
    • c.Provide(constructor, dig.Name("xxx")) 如果 constructor 存在多个返回值,则所有的值都将命名为 xxx,也就是说 name 是类型下的一个属性,不要求全局唯一
    • c.Provide(newIntAndString, dig.Name("a"), dig.Name("b")) 若存在多个 name option,以最后一个为准
    • 结构体 tag name 可以和 optional 共同使用,如果没有 optional,且容器中没有将报错
  • 组,注册的构造函数的返回值存在相同类型时,可以将这些组织成一个 List。使用组的注意事项如下:
    • 同一个函数里返回同一类型的多个值,可以用 group
    • 同一个函数里返回不同类型的多个值,也可以使用 group
    • 在 group 在注入过程中,如果没有相关 Provider,则注入一个 nil 类型
    • 注入过程中,不保证 group 有序
    • 在结果对象中,使用 group 标签时,可以使用 flatten 将一个切片展平
  • 注意事项
    • 针对同一个值,命名值和组只能使用一种
    • 命名值和组是 value 类型下面的一个属性,不要求全局唯一,换句话来说,一个 value 在容器中的 key 为 type + ?name + ?group。
    • 命名值和组,在 Invoke 中只能通过 参数对象 和 结构体 tag (namegroup) 的方式使用,无法直接使用

dig/name_and_group.go

package main

import (
	"fmt"
	"log"

	"go.uber.org/dig"
)

func newInt() int {
	return -1
}

func newInt0And1() (int, int) {
	return 0, 1
}

func newIntAndString() (int, string) {
	return 1, "a"
}

func newInt2() int {
	return 2
}

func newInt3() int {
	return 3
}

func newItemA() string {
	return "a"
}

type Int3AndItemBOut struct {
	dig.Out
	Int4      int      `name:"int4"`
	ItemB     string   `group:"list"`
	ItemOther []string `group:"list,flatten"` // 注意展平
}

func newItemCD() (string, string) {
	return "c", "d"
}

func newInt4AndItemBOut() Int3AndItemBOut {
	return Int3AndItemBOut{
		Int4:      4,
		ItemB:     "b",
		ItemOther: []string{"e", "f", "g"},
	}
}

type NameAndGroupIn struct {
	dig.In
	Int0 int      `name:"int0" optional:"true"`
	Int1 int      `name:"int1" optional:"true"`
	Int2 int      `name:"int2"`
	Int3 int      `name:"int3"`
	Int4 int      `name:"int4"`
	List []string `group:"list"`
}

type IntStringGroup struct {
	dig.In
	ListInt        []int    `group:"list_int"`
	ListIntString1 []int    `group:"list_int_string"`
	ListIntString2 []string `group:"list_int_string"`
	ListString     []string `group:"list_string"` // 允许不存在
}

func RunNameAndGroupError1() {
	c := dig.New()
	// 注册构造函数
	errs := []error{
		c.Provide(newInt0And1),
		c.Provide(newInt0And1, dig.Name("int0")),
		c.Provide(newInt0And1, dig.Name("int0"), dig.Name("int0")),
	}
	// 错误处理
	for i, err := range errs {
		if err != nil {
			fmt.Println(c)
			fmt.Printf("错误场景1[%d] - 不支持在同一个函数里返回同一类型的多个值: %s\n", i, err)
		}
	}
}

func RunNameAndGroupError2() {
	c := dig.New()
	// 注册构造函数
	errs := []error{
		c.Provide(newInt2, dig.Name("int2")),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			log.Fatal(err)
		}
	}
	err := c.Invoke(func(int_ int) {
		fmt.Printf("int: %d\n", int_)
	})
	if err != nil {
		fmt.Println(c)
		fmt.Println("错误场景2 - 命名值不能注入到非命令名参数中:", err)
	}
}

func RunNameAndGroupError3() {
	c := dig.New()
	// 注册构造函数
	errs := []error{
		c.Provide(newInt2, dig.Name("int2")),
		c.Provide(newInt2, dig.Name("int2")),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			fmt.Printf("错误场景3 - 不允许同一类型存在多个相同的命名 %s\n", err)
		}
	}
}

func RunNameReturnMultipleAndNameMultiple() {
	c := dig.New()
	err := c.Provide(newIntAndString, dig.Name("a"), dig.Name("b"))
	if err != nil {
		fmt.Println(c)
		log.Fatal(err)
	}
	fmt.Println(c)
	fmt.Println("RunNameReturnMultipleAndNameMultiple: int 和 string 每个值都会被命名成 b")
}

func RunGroupReturnTypeGroup() {
	c := dig.New()

	// 注册构造函数
	errs := []error{
		c.Provide(newInt0And1, dig.Group("list_int")),
		c.Provide(newIntAndString, dig.Group("list_int_string")),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			log.Fatal(err)
		}
	}
	err := c.Invoke(func(IntGroup IntStringGroup) {
		fmt.Printf("RunGroupReturnSameTypeGroup: list_int=%#v\n", IntGroup)
	})
	if err != nil {
		fmt.Println(c)
		log.Fatalln(err)
	}
	fmt.Println(c)
	fmt.Println("RunGroupReturnSameTypeGroup: 构造函数返回想同类型可以使用 group 进行聚合")
}

func RunNameAndGroup() {

	RunNameAndGroupError1()
	RunNameAndGroupError2()
	RunNameAndGroupError3()
	RunNameReturnMultipleAndNameMultiple()
	RunGroupReturnTypeGroup()

	c := dig.New()

	// 注册构造函数
	errs := []error{
		c.Provide(newInt),
		c.Provide(newInt2, dig.Name("int2")),
		c.Provide(newInt3, dig.Name("int3")),
		c.Provide(newInt4AndItemBOut),
		c.Provide(newItemA, dig.Group("list")),
		c.Provide(newItemCD, dig.Group("list")),
	}
	// 错误处理
	for _, err := range errs {
		if err != nil {
			fmt.Println(c)
			log.Fatal(err)
		}
	}
	// 调用函数,并将 bean 注入函数参数
	err := c.Invoke(func(_int int, in NameAndGroupIn) {
		fmt.Printf("RunNameAndGroup: int=%d, in=%#v\n", _int, in)
	})
	if err != nil {
		fmt.Println(c)
		log.Fatalln(err)
	}
	fmt.Println(c.String())
}

dig/main.go

	RunNameAndGroup()

缺点

  • 运行进行分析和注入,不利于静态分析,不利于尽早暴露问题
  • 某些高级特性(比如:参数对象、结果对象等),对业务代码有一定的侵入性
  • 众多 option 配置复杂,case 很多,接口不太直观,相对难以理解一些

fx

fx 是对 dig 库做的一层封装,以框架的方式提供能力,并添加了 App 生命周期管理,日志等相关能力。

添加依赖

go get go.uber.org/fx@v1

例子

创建包目录 mkdir fx

fx/sample.go

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/rectcircle/go-dependency-injection-learn/bean/sample"
	"go.uber.org/fx"
	"go.uber.org/fx/fxevent"
)

type SampleIn struct {
	fx.In

	A *sample.A
	B *sample.B
	C *sample.C
}

func RunSample(a string, b int) {

	var sampleC *sample.C
	var sampleIn SampleIn
	var g fx.DotGraph

	app := fx.New(
		// fx.NopLogger, // 关闭日志
		// 配置 fx 框架的日志
		fx.WithLogger(
			func() fxevent.Logger {
				// 默认 log 如下所示
				return &fxevent.ConsoleLogger{W: os.Stderr}
			},
		),
		// fx.ErrorHook(), // 错误处理

		// fx.Supply 等价于 fx.Provide(func() (string, int) { return a, b })
		fx.Supply(a, b),
		// 和 dig 用法类似, dig.Option 能力需通过 fx.Annotated 实现,支持参数对象和结果对象
		// fx.Lifecycle 可以作为构造函数参数
		fx.Provide(sample.NewA, sample.NewB, sample.NewC,
			// 如果想使用命名值和组,可以通过 fx.Annotated 包裹一下
			// 目前还不支持 dig.As 类似的接口绑定
			fx.Annotated{
				Name:   "namedC",
				Target: sample.NewC,
			}),
		// 和 dig 用法类似
		// fx.New 执行完成后,Invoke 就会被调用完成,支持参数对象和结果对象
		// fx.Lifecycle Invoke 函数的参数
		fx.Invoke(func(lc fx.Lifecycle, c *sample.C) {
			fmt.Printf("RunSample - Invoke: %s\n", c)
			// fx.Hook 事件函数,不允许阻塞,默认超时为 fx.DefaultTimeout (15 s)
			// 可以通过 fx.StartTimeout() 和 fx.StopTimeout() 配置
			lc.Append(fx.Hook{
				// 启动回调函数
				OnStart: func(context.Context) error {
					fmt.Printf("RunSample - hooks[0] - OnStart: %s\n", c)
					return nil
				},
				// 停止回调函数
				OnStop: func(context.Context) error {
					fmt.Printf("RunSample - hooks[0] - OnStop: %s\n", c)
					return nil
				},
			})
			// 存在多个,onStart 按照 append 的顺序调用,onSop 按照 append 的逆序调用
			lc.Append(fx.Hook{
				// 启动回调函数
				OnStart: func(context.Context) error {
					fmt.Printf("RunSample -  hooks[1] - OnStart: %s\n", c)
					return nil
				},
				// 停止回调函数
				OnStop: func(context.Context) error {
					fmt.Printf("RunSample -  hooks[1] - OnStop: %s\n", c)
					return nil
				},
			})
		}),
		// 将容器内的通类型的对象赋值给变量,注意,必须是容器内对象的指针类型。也就是说:
		// 如果容器内是 struct 类型,这里传递的是 *struct
		// 如果容器内是 *struct,这里传递的就是**struct
		fx.Populate(&sampleC),
		fx.Populate(&sampleIn), // 不支持 参数对象
		fx.Populate(&g),        // 拿到 DotGraph
	)

	fmt.Printf("RunSample - Populate *sample.C: %s\n", sampleC)
	fmt.Printf("RunSample - Populate sampleIn: %#v\n", sampleIn)
	fmt.Printf("RunSample - DotGraph: \n%s\n", g)

	// app.Run()
	err := app.Start(context.Background())
	fmt.Println(err)
	err = app.Stop(context.Background())
	fmt.Println(err)
}

fx/main.go

package main

func main() {
	RunSample("a", 1)
}

核心 API

  • fx.New 创建一个 fx app,支持如下 Option
    • 日志: fx.WithLogger 自定义 fx 内部日志,fx.NopLogger 禁用日志,默认打到 stdout 中
    • 错误和错误处理: fx.Errorfx.ErrorHook
    • 提供外部参数 fx.Supply
    • 注册构造函数 fx.Provide,可通过 fx.Annotated 实现 dig.Option 的能力,可以使用 lc fx.Lifecycle 作为参数注入 Hook
    • 执行并注入 fx.Invoke,可以使用 lc fx.Lifecycle 作为参数注入 Hook
    • 超时 fx.StartTimeoutfx.StopTimeout,默认为 fx.DefaultTimeout 15 秒
    • fx.Populate 将依赖注入容器内的值赋值给外部变量,注意,必须是容器内对象的指针类型。也就是说:
      • 如果容器内是 struct 类型,这里传递的是 *struct
      • 如果容器内是 *struct,这里传递的就是 **struct
  • fx.Lifecyclefx.Hook 为 fx 生命周期回调,fx.Lifecycle 可以作为 fx.Providefx.Invoke 的参数
  • fx.DotGraph 可通过 fx.Populate 获取到
  • app.Run 阻塞运行
  • app.Start 启动
  • app.Stop 停止

原理

对 dig 进行封装

缺点

  • 包含 dig 的所有缺点
  • 暂时没有 dig.As 的能力,无法进行接口绑定

go-spring

  • version: v1.0.5

添加依赖

go get github.com/go-spring/go-spring

例子

创建包目录 mkdir go-spring

go-spring/sample.go

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/go-spring/spring-core/gs"
	"github.com/rectcircle/go-dependency-injection-learn/bean/sample"
)

type SampleApp struct {
	C  *sample.C `autowire:""`
	D2 *D2       `autowire:""`
}

type D1 struct {
	D2 *D2 `autowire:""`
}

type D2 struct {
	D1 *D1 `autowire:""`
}

func (a *SampleApp) OnStartApp(e gs.Environment) {
	fmt.Printf("RunSample - gs.AppEvent - OnStartApp: e=%v, c=%s, d2=%v\n", e, a.C, a.D2)
	gs.ShutDown(errors.New(""))
}

func (a *SampleApp) OnStopApp(ctx context.Context) {
	fmt.Printf("RunSample - gs.AppEvent - OnStopApp: %v\n", ctx)
}

func RunSample(a string, b int) {
	// 按照官方的 demo,`gs.Object` 以及 `gs.Provide` 应定义处的 init 函数中

	// go-spring 对 bean 的定义为:一个变量赋值给另一个变量后二者指向相同的内存地址(指针类型)
	// 因此 bean 只有这四种 ptr、interface、chan、func
	gs.Object(&a) // gs.Object(a) // 这种写将报错
	gs.Object(&b) // gs.Object(b) // 这种写法报错

	gs.Provide(func(a *string) *sample.A { return sample.NewA(*a) })
	gs.Provide(func(b *int) *sample.B { return sample.NewB(*b) })
	// 如下两种写法不对,会找不到类型
	// gs.Provide(sample.NewA)
	// gs.Provide(sample.NewB)

	// 循环引用测试
	gs.Object(&D1{})
	gs.Object(&D2{})

	gs.Provide(sample.NewC)

	// 注册 AppEvent
	gs.Provide(new(SampleApp)).Export(new(gs.AppEvent))
	err := gs.Run()
	if err != nil {
		log.Fatal(err)
	}
}

go-spring/main.go

package main

func main() {
	RunSample("a", 1)
}

缺点

  • 只有中文社区,且没有系统的文档
  • 对标 Java Spring 框架,是一种大而全的框架,比较重,不符合 Go 的设计哲学
  • API 尚未稳定,不符合 Go 语义化版本的规范,名义 major 在 1,但是已经发生不兼容的情况
  • API 缺乏设计,公开 API 不够闭环,比如没有 Start Stop 等函数
  • 初始化阶段异常直接 panic
  • 单元测试不足
  • 值类型无法放入 Bean 容器中,只有 ptr、interface、chan、func 四种类型可以作为 Bean 容器

对比

仓库google/wireuber-go/diguber-go/fxgo-spring/go-spring
stars(截止 2021-10-03)6.6k2.1K2.3k930
维护组织googleuberuberdidi
原理编译时(代码生成+条件编译)运行时(反射)运行时(反射)运行时(反射)
库/框架框架框架
构造器注入
属性注入
提供常量值
接口绑定?
对象命名?
对象组(注入多个同类型对象组成一个切片)?
循环依赖
侵入性3(需要定义很多基础类型)444
测试覆盖度60%98%98%无数据
轻量级5542
文档5551
社区5553

如何选择

首先,排除掉 go-spring 在生产环境使用。在以上对比中表现较差。

剩余三个质量均比较高

  • 如果想使用编译时依赖注入,使用 wire 是最好的选择
  • 如果想使用运行时依赖植入,且只想用依赖注入特性,dig 是较好的选择
  • 如果需求不仅仅是依赖注入,而是想找一个轻量级 App 框架,name fx 可以满足需求

但是,这三个依赖注入库均存在能力不足的问题。因此,基于 dig,开发了 digpro。

更多参见: digpro