在现代软件开发中,依赖注入(Dependency Injection,简称 DI)已经成为一种广泛采用的设计模式。具体的做法可以遵守一个重要的设计准则:所有依赖应该在组件初始化时传递给它,这就是依赖注入(Dependency injection)。
在众多工具中,Wire 以其简洁、强大和易用性脱颖而出,成为 Go 语言项目中的宠儿。
1. 介绍
正式开始前需要先了解一下 wire 当中的两个概念:provider 和 injector。
1.1 Provider
Provider 是一个普通的函数,这个函数会返回构建依赖关系所需的组件。如下所示,就是一个 provider 函数,在实际使用的时候,往往是一些简单的工厂函数,这个函数不会太复杂。
1 2
| func NewPostRepo() IPostRepo {}
|
不过需要注意的是在 wire 中不能存在两个 provider 返回相同的组件类型。
1.2 Injector
injector 也是一个普通函数,我们常常在 wire.go
文件中定义 injector 函数签名,然后通过 wire
命令自动生成一个完整的函数。
1 2 3 4 5
|
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 示例
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
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
package example import "github.com/google/wire" func GetPostService() *PostService { panic(wire.Build( NewPostService, NewPostUsecase, NewPostRepo, )) }
|
在 wire.go 文件夹下,wire . 生成文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
package example
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
| func NewPostService(u IPostUsecase) (*PostService, error) { return &PostService{usecase: u}, nil }
func GetPostService() (*PostService, error) { panic(wire.Build( NewPostService, NewPostUsecase, NewPostRepo, )) }
|
生成的代码如下所示,可以发现会像我们自己写代码一样判断一下 if err
然后返回
1 2 3 4 5 6 7 8 9 10 11
|
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
| func NewPostRepo() (IPostRepo, func(), error) { return new(IPostRepo), nil, nil }
func NewPostUsecase(repo IPostRepo) (IPostUsecase, func(), error) { return postUsecase{repo: repo}, nil, nil }
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
|
package example
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),
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
|
- 一般在 cmd/xxx 目录下创建 wire.go 用于构建 injector 函数签名,因为我们一般会在 main 当中构建依赖关系完成服务启动。
- 在 internal 或者是 internal/app 目录下创建
wire_set.go
构建 ProviderSet
,这里要注意- 这里的
ProviderSet
中的 Provider
函数只能是当前目录下创建的 Provider 函数 - 例如可能存在 usecase 和 repo 都依赖 config 如果 repo 创建一个 ProviderSet 包含
NewConfig
,usecase 也来一个,就会导致在 wire .
生成代码的时候报错,因为有冲突,同一个组件有两个 Provider
4. 参考资料