16 分钟
Go 依赖注入库调研与体验
介绍
参考 依赖注入 一文
调研范围
本文将调研如下几个比较主流的 Go 依赖注入库和框架。
仓库 | stars(截止 2021-10-03) |
---|---|
google/wire | 6.6k |
uber-go/dig | 2.1K |
uber-go/fx | 2.3k |
go-spring/go-spring | 930 |
实验项目
创建
创建一个实现项目 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
参见:官方
缺点
- 由于其实现限制,类型不能相同,需要定义许多额外的基础类型别名
- 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
一起使用 vdig.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 (name
、group
) 的方式使用,无法直接使用
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
- version: v1.14.2
- go docs
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.Error
和fx.ErrorHook
- 提供外部参数
fx.Supply
- 注册构造函数
fx.Provide
,可通过fx.Annotated
实现 dig.Option 的能力,可以使用lc fx.Lifecycle
作为参数注入 Hook - 执行并注入
fx.Invoke
,可以使用lc fx.Lifecycle
作为参数注入 Hook - 超时
fx.StartTimeout
与fx.StopTimeout
,默认为fx.DefaultTimeout
15 秒 fx.Populate
将依赖注入容器内的值赋值给外部变量,注意,必须是容器内对象的指针类型。也就是说:- 如果容器内是
struct
类型,这里传递的是*struct
- 如果容器内是
*struct
,这里传递的就是**struct
- 如果容器内是
- 日志:
fx.Lifecycle
和fx.Hook
为 fx 生命周期回调,fx.Lifecycle
可以作为fx.Provide
和fx.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/wire | uber-go/dig | uber-go/fx | go-spring/go-spring |
---|---|---|---|---|
stars(截止 2021-10-03) | 6.6k | 2.1K | 2.3k | 930 |
维护组织 | uber | uber | didi | |
原理 | 编译时(代码生成+条件编译) | 运行时(反射) | 运行时(反射) | 运行时(反射) |
库/框架 | 库 | 库 | 框架 | 框架 |
构造器注入 | ✅ | ✅ | ✅ | ✅ |
属性注入 | ✅ | ❌ | ❌ | ✅ |
提供常量值 | ✅ | ❌ | ✅ | ✅ |
接口绑定 | ✅ | ✅ | ❌ | ? |
对象命名 | ❌ | ✅ | ✅ | ? |
对象组(注入多个同类型对象组成一个切片) | ❌ | ✅ | ✅ | ? |
循环依赖 | ❌ | ❌ | ❌ | ✅ |
侵入性 | 3(需要定义很多基础类型) | 4 | 4 | 4 |
测试覆盖度 | 60% | 98% | 98% | 无数据 |
轻量级 | 5 | 5 | 4 | 2 |
文档 | 5 | 5 | 5 | 1 |
社区 | 5 | 5 | 5 | 3 |
如何选择
首先,排除掉 go-spring 在生产环境使用。在以上对比中表现较差。
剩余三个质量均比较高
- 如果想使用编译时依赖注入,使用 wire 是最好的选择
- 如果想使用运行时依赖植入,且只想用依赖注入特性,dig 是较好的选择
- 如果需求不仅仅是依赖注入,而是想找一个轻量级 App 框架,name fx 可以满足需求
但是,这三个依赖注入库均存在能力不足的问题。因此,基于 dig,开发了 digpro。
更多参见: digpro