0%

golang的依赖注入库wire

在现代软件开发中,依赖注入(Dependency Injection,简称 DI)已经成为一种广泛采用的设计模式。具体的做法可以遵守一个重要的设计准则:所有依赖应该在组件初始化时传递给它,这就是依赖注入(Dependency injection)。

在众多工具中,Wire 以其简洁、强大和易用性脱颖而出,成为 Go 语言项目中的宠儿。

1. 介绍

正式开始前需要先了解一下 wire 当中的两个概念:provider 和 injector。

1.1 Provider

Provider 是一个普通的函数,这个函数会返回构建依赖关系所需的组件。如下所示,就是一个 provider 函数,在实际使用的时候,往往是一些简单的工厂函数,这个函数不会太复杂。

1
2
// NewPostRepo 创建文章 Repo
func NewPostRepo() IPostRepo {}

不过需要注意的是在 wire 中不能存在两个 provider 返回相同的组件类型。

1.2 Injector

injector 也是一个普通函数,我们常常在 wire.go  文件中定义 injector 函数签名,然后通过 wire  命令自动生成一个完整的函数。

1
2
3
4
5
//+build wireinject

func GetBlogService() *Blog {
panic(wire.Build(NewBlogService, NewPostUsecase, NewPostRepo))
}

第一行的 //+build wireinject 注释确保了这个文件在我们正常编译的时候不会被引用,而 wire . 生成的文件 wire_gen.go 会包含 //+build !wireinject 注释,正常编译的时候,不指定 tag 的情况下会引用这个文件

wire.Build 在 injector 函数中使用,用于表明这个 injector 由哪些 provider 提供依赖, injector 函数本身只是一个函数签名,所以我们直接在函数中 panic 实际生成代码的时候并不会直接调用 panic

2. 使用

2.1 安装

1
2
3
4
# 安装
go install github.com/google/wire/cmd/wire@latest
# 使用
wire .

2.2 示例

  • example.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
package example  

type IPostRepo interface{}

func NewPostRepo() IPostRepo {
return new(IPostRepo)
}

type IPostUsecase interface{}
type postUsecase struct {
repo IPostRepo
}

func NewPostUsecase(repo IPostRepo) IPostUsecase {
return postUsecase{repo: repo}
}

type PostService struct {
usecase IPostUsecase
}

func NewPostService(u IPostUsecase) *PostService {
return &PostService{usecase: u}
}

NewPostService NewPostUsecase  这些都是 Provider  函数

  • wire.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//go:build wireinject  
// +build wireinject

package example

import "github.com/google/wire"

func GetPostService() *PostService {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}

在 wire.go 文件夹下,wire . 生成文件。

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

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

package example

// Injectors from wire.go:

func GetPostService() *PostService {
iPostRepo := NewPostRepo()
iPostUsecase := NewPostUsecase(iPostRepo)
postService := NewPostService(iPostUsecase)
return postService
}

2.3 返回错误

在 go 中如果遇到错误,我们会在最后一个返回值返回 error,wire 同样也支持返回错误的情况,只需要在 injector 的函数签名中加上 error 返回值即可,还是前面的那个例子,我们让 NewPostService  返回 error,并且修改 GetPostService  这个 Injector  函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
// example.go
func NewPostService(u IPostUsecase) (*PostService, error) {
return &PostService{usecase: u}, nil
}

// wire.go
func GetPostService() (*PostService, error) {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}

生成的代码如下所示,可以发现会像我们自己写代码一样判断一下 if err  然后返回

1
2
3
4
5
6
7
8
9
10
11
// wire_gen.go

func GetPostService() (*PostService, error) {
iPostRepo := NewPostRepo()
iPostUsecase := NewPostUsecase(iPostRepo)
postService, err := NewPostService(iPostUsecase)
if err != nil {
return nil, err
}
return postService, nil
}

2.4 清理函数

有时候我们需要打开文件,或者是链接这种需要关闭的资源,这时候 provider 可以返回一个闭包函数 func() ,wire 在进行构建的时候,会在报错的时候调用,并且会将所有的闭包函数聚合返回。

我们修改一下 NewPostRepo NewPostUsecase  让他们返回一个清理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// example.go
func NewPostRepo() (IPostRepo, func(), error) {
return new(IPostRepo), nil, nil
}


func NewPostUsecase(repo IPostRepo) (IPostUsecase, func(), error) {
return postUsecase{repo: repo}, nil, nil
}

// wire.go
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}

再次生成,当 NewPostUsecase  出现错误的时候会自动帮我们调用 NewPostRepo  返回的 cleanup  函数,而 NewPostService  返回错误,会调用它依赖的所有 provider 的 cleanup 函数,如果都没有问题,就会把所有 cleanup 函数聚合为一个函数返回。

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
// Code generated by Wire. DO NOT EDIT.  

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

package example

// Injectors from wire.go:

func GetPostService() (*PostService, func(), error) {
iPostRepo, cleanup, err := NewPostRepo()
if err != nil {
return nil, nil, err
}
iPostUsecase, cleanup2, err := NewPostUsecase(iPostRepo)
if err != nil {
cleanup()
return nil, nil, err
}
postService, err := NewPostService(iPostUsecase)
if err != nil {
cleanup2()
cleanup()
return nil, nil, err
}
return postService, func() {
cleanup2()
cleanup()
}, nil
}

3. Wire 使用最佳实践

3.1 不要使用默认类型

之前有提到过,wire 不支持两个提供两个相同类型的 provider,所以如果我们使用默认类型如 int string  等,只要有两个依赖就会导致报错,解决方案是使用类型别名。
先来看一个报错的示例

1
2
3
4
5
6
type PostService struct {
usecase IPostUsecase
a int
b int
r io.Reader
}

可以看到,wire 在构建依赖关系的时候,并不知道 int 的值该分配给 a 还是 b 所以就会报错,我们自定义两个类型就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type A int
type B int


type PostService struct {
usecase IPostUsecase
a A
b B
r io.Reader
}

var PostServiceSet = wire.NewSet(
wire.Struct(new(PostService), "*"),
wire.Value(A(10)),
wire.Value(B(10)),
wire.InterfaceValue(new(io.Reader), os.Stdin),
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
)

这种方式在使用上会感觉有点糟心,但是就我目前的使用来看,用到基础类型的情况还是比价少,所以也还好。

3.2 Option Struct

在实际的业务场景当中我们的 NewXXX 函数的参数列表可能会很长,这个时候就可以直接定义一个 Option Struct 然后使用 wire.Strcut 来构建 Option Strcut 的依赖。

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
type PostUsecaseOption struct {
a A
b B
repo IPostRepo
}


func NewPostUsecase(opt *PostUsecaseOption) (*PostUsecase, func(), error) {
return &PostUsecase{repo: opt.repo}, nil, nil
}


var PostServiceSet = wire.NewSet(
wire.Struct(new(PostService), "*"),
wire.Value(A(10)),
wire.Value(B(10)),
wire.InterfaceValue(new(io.Reader), os.Stdin),

// for usecase
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
wire.Struct(new(PostUsecaseOption), "*"),
NewPostUsecase,

NewPostRepo,
)

3.3 项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── api
├── cmd
└── app
├── main.go
├── wire.go
└── wire_gen.go
└── internal
├── domain
└── post.go
├── repo
└── repo.go
├── service
└── service.go
├── usecase
└── usecase.go
└── wire_set.go
  1. 一般在 cmd/xxx 目录下创建 wire.go 用于构建 injector 函数签名,因为我们一般会在 main 当中构建依赖关系完成服务启动。
  2. 在 internal 或者是 internal/app 目录下创建 wire_set.go  构建 ProviderSet ,这里要注意
    • 这里的 ProviderSet  中的 Provider  函数只能是当前目录下创建的 Provider 函数
    • 例如可能存在 usecase 和 repo 都依赖 config 如果 repo 创建一个 ProviderSet 包含 NewConfig ,usecase 也来一个,就会导致在 wire .  生成代码的时候报错,因为有冲突,同一个组件有两个 Provider

4. 参考资料

可以加首页作者微信,咨询相关问题!