Go工程化--依赖注入(一)
1. 前言
1.1 什么是依赖注入?
为了让代码可测或者可以运行,将各个module的顺序按业务逻辑进行初始化,比如首先进行config
初始化,再初始化log
模块,再然后是数据库
,再然后实例化客户端
, 这个过程其实就是依赖注入,而通常我们会在main.go做依赖注入,这就导致main.go会越来越臃肿。
在main.go中做依赖注入,意味着在初始化代码中我们要管理:
- 依赖的初始化顺序
- 依赖之间的关系
对于小型项目而言,依赖的数量比较少,初始化代码不会很多,不需要引入依赖注入框架。但对于依赖较多的中大型项目,初始化代码又臭又长,可读性和维护性变的很差,如下一段启动函数:
func main() {
config := NewConfig()
// db依赖配置
db, err := ConnectDatabase(config)
if err != nil {
panic(err)
}
// PersonRepository 依赖db
personRepository := NewPersonRepository(db)
// PersonService 依赖配置 和 PersonRepository
personService := NewPersonService(config, personRepository)
// NewServer 依赖配置和PersonService
server := NewServer(config, personService)
server.Run()
}
1.2 为什么要做使用依赖注入框架?
随着开发合作的同学的增多以及部门的要求增加,项目启动时的依赖越来越多,依赖之间还有先后顺序,有一些甚至是隐式的顺序,到时main函数的代码膨胀的非常迅速并且慢慢的变的不可维护了,这种情况下引入依赖注入框架其实可以省心很多。
1.3 依赖注入框架分类
Golang 的依赖注入框架有两类
- 一类是通过反射在运行时进行依赖注入,典型代表是 uber 开源的dig和Facebook的 [inject] (https://github.com/facebookarchive/inject)
- 另外一类是通过 generate 进行代码生成,典型代表是 Google 开源的 wire。
使用 dig 功能会强大一些,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection), 但是缺点就是错误只能在运行时才能发现,这样如果不小心的话可能会导致一些隐藏的 bug 出现。
wire则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection), 使用 wire 的缺点就是功能限制多一些,但是好处就是编译的时候就可以发现问题,并且生成的代码其实和我们自己手写相关代码差不太多,更符合直觉,心智负担更小,十分容易理解和调试。所以更加推荐 wire.
2. wire
2.0 安装
安装很简单,只要安装了 Go 并且已经把 $GOPATH/bin
加入到了 PATH 当中,终端执行下面的语句即可
go get github.com/google/wire/cmd/wire
2.1 如何工作?
wire有两个基本的概念:provider
和injector
2.2 provider
provider就是普通的Go函数,可以把它看作是某对象的 构造函数,通过provider告诉wire该对象的依赖情况:
// NewUserStore是*UserStore的provider,表明*UserStore依赖于*Config和 *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig是*Config的provider,没有依赖
func NewDefaultConfig() *Config {...}
// NewDB是*mysql.DB的provider,依赖于ConnectionInfo
func NewDB(info ConnectionInfo) (*mysql.DB, error) {...}
// UserStoreSet 可选项,可以使用wire.NewSet将通常会一起使用的依赖组合起来。
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)
不过需要注意的是在wire 中不能存在两个 provider 返回相同的组件类型
2.3 Injector
injector 也是一个普通函数,常常在 wire.go 文件中定义 injector
函数签名,通过调用injector
来获取我们所需的对象或值,injector
会按照依赖关系,按顺序调用provider
函数, 通过 wire 命令自动生成一个完整的函数:
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
// initUserStore是由wire生成的injector
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// *Config的provider函数
defaultConfig := NewDefaultConfig()
// *mysql.DB的provider函数
db, err := NewDB(info)
if err != nil {
return nil, err
}
// *UserStore的provider函数
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
- 第一行的
//+build wireinject
注释确保了这个文件在我们正常编译的时候不会被引用,而wire .
生成的文件wire_gen.go
会包含//+build !wireinject
注释,正常编译的时候,不指定 tag 的情况下会引用这个文件
injector帮我们把按顺序初始化依赖的步骤给做了,我们在main.go
中只需要调用initUserStore
方法就能得到我们想要的对象了。
那么wire是怎么知道如何生成injector的呢?我们需要在wire.go
文件中写一个签名函数来告诉它:
- 定义injector的函数签名
- 在函数中使用
wire.Build
方法列举生成injector所需的provider
例如签名函数initUserStore
:
// initUserStore用于声明injector的函数签名
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// wire.Build声明要获取一个UserStore需要调用到哪些provider函数
wire.Build(UserStoreSet, NewDB)
// 这些返回值wire并不关心
return nil, nil
}
有了上面的函数,wire就可以得知如何生成injector了。wire生成injector的步骤描述如下:
- 确定所生成injector函数的函数签名:
func initUserStore(info ConnectionInfo) (*UserStore, error)
- 感知返回值第一个参数是
*UserStore
- 检查
wire.Build
列表,找到*UserStor
e的provider:NewUserStore
- 由函数签名
func NewUserStore(cfg *Config, db *mysql.DB)
得知NewUserStore依赖于*Config
, 和*mysql.DB
- 检查
wire.Build
列表,找到*Config
和*mysql.DB
的provider:NewDefaultConfig
和NewDB
- 由函数签名
func NewDefaultConfig() *Config
得知*Config
没有其他依赖了。 - 由函数签名
func NewDB(info *ConnectionInfo) (*mysql.DB, error)
得知*mysql.DB
依赖于ConnectionInfo
。 - 检查
wire.Build
列表,找不到ConnectionInfo的provider
,但在injector
函数签名中发现匹配的入参类型,直接使用该参数作为NewDB的入参。 - 感知返回值的第二个参数
error
- ...
- 按依赖关系。按顺序调用
privider
函数, 拼装injector
函数
2.4 一个完整的 🌰
provider.go
:
// provider.go
package example
// repo
// IPostRepo IPostRepo
type IPostRepo interface{}
// NewPostRepo NewPostRepo
func NewPostRepo() IPostRepo {
return new(IPostRepo)
}
// usecase
// IPostUsecase IPostUsecase
type IPostUsecase interface{}
type postUsecase struct {
repo IPostRepo
}
// NewPostUsecase NewPostUsecase
func NewPostUsecase(repo IPostRepo) IPostUsecase {
return postUsecase{repo: repo}
}
// service service
// PostService PostService
type PostService struct {
usecase IPostUsecase
}
// NewPostService NewPostService
func NewPostService(u IPostUsecase) *PostService {
return &PostService{usecase: u}
}
NewPostService
,NewPostUsecase
这些都是 Provider 函数,下面我们在 wire.go
当中构建 Injector 函数签名
- 签名文件
wire.go
中的签名函数
//+build wireinject
package example
import "github.com/google/wire"
func GetPostService() *PostService {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}
- 当前(
wire.go
)执行指令wire .
生成依赖函数,(以下代码是自动生成的)
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package example
// Injectors from wire.go:
func GetPostService() *PostService {
iPostRepo := NewPostRepo()
iPostUsecase := NewPostUsecase(iPostRepo)
postService := NewPostService(iPostUsecase)
return postService
}
2.5 错误机制
缺少provider
在执行wire .
的时候,如果缺少某个Provider
提供依赖,wire会进行提示,帮助我们快速找到问题且修改。
还是上面的例子,我们删除掉一个Provider函数试试
func GetPostService() *PostService {
panic(wire.Build(
NewPostService,
NewPostUsecase,
))
}
再次执行 wire
命令,可以发现报错
▶ wire .
wire: /Go-000/Week04/blog/03_wire/01_example/wire.go:7:1: inject GetPostService: no provider found for github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.IPostRepo
needed by github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.IPostUsecase in provider "NewPostUsecase" (/Go-000/Week04/blog/03_wire/01_example/example.go:22:6)
needed by *github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.PostService in provider "NewPostService" (/Go-000/Week04/blog/03_wire/01_example/example.go:34:6)
wire: github.com/mohuishou/go-training/Week04/blog/03_wire/01_example: generate failed
wire: at least one generate failure
2.6 返回错误
在go
中如果遇到错误,会在最后一个返回值返回error
,wire
同样也支持返回错误的情况,只需要在 injector
的函数签名中加上 error
返回值即可,还是前面的那个例子,我们让 NewPostService
返回error
,并且修改 GetPostService
这个Injector
函数
// example.go
// NewPostService NewPostService
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
然后返回
// 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.7 清理函数
有时候需要打开文件,或者是链接这种需要关闭的资源,这时候provider
可以返回一个闭包函数 func(),wire
在进行构建的时候,会在报错的时候调用,并且会将所有的闭包函数聚合返回。这个特性一般用的不多,但是有需求的时候会十分有用。
还是之前的示例,我们修改一下 NewPostRepo NewPostUsecase 让他们返回一个清理函数
// example.go
// NewPostRepo NewPostRepo
func NewPostRepo() (IPostRepo, func(), error) {
return new(IPostRepo), nil, nil
}
// NewPostUsecase NewPostUsecase
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,
))
}
执行 wire .
之后可以发现生成的函数当中,当 NewPostUsecase
出现错误的时候会自动调用 NewPostRepo
返回的 cleanup
函数,而 NewPostService
返回错误,会调用它依赖的所有 provider
的 cleanup
函数,如果都没有问题,就会把所有cleanup
函数聚合为一个函数返回
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. 高级方法
3.1 接口注入
应该依赖接口(interface
),而不是实现(struct
)。返回数据的时候返回实现(struct
)而不是接口(interface
),这是在 Golang 中的最佳实践(当然也不是所有的都是这样),所以如果我们的 provider
返回了实现,但是我们的依赖的是接口,这时候就会报错了,先来看一个例子。
修改一下NewPostUsecase
方法,让他返回 *PostUsecase
而不是接口
// NewPostUsecase NewPostUsecase
func NewPostUsecase(repo IPostRepo) (*PostUsecase, func(), error) {
return &PostUsecase{repo: repo}, nil, nil
}
这时候执行 wire .
生成代码会发现报错,找不到 IPostUsecase
的 provider
▶ wire .
wire: /Go-000/Week04/blog/03_wire/01_example/wire.go:7:1: inject GetPostService: no provider found for github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.IPostUsecase
needed by *github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.PostService in provider "NewPostService" (/Go-000/Week04/blog/03_wire/01_example/example.go:36:6)
wire: github.com/mohuishou/go-training/Week04/blog/03_wire/01_example: generate failed
wire: at least one generate failure
这时候就需要使用 wire.Bind
将 Struct
和接口进行绑定了,表示这个结构体实现了这个接口,我们修改一下 injector 函数
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
NewPostService,
wire.Bind(new(IPostUsecase), new(*PostUsecase)), // struct 和接口进行绑定
NewPostUsecase,
NewPostRepo,
))
}
wire.Bind
的使用方法就是 wire.Bind(new(接口), new(实现))
3.2 Struct 属性注入
在上面 NewPostService
代码,我们可以发现有很多 Struct 的初始化其实就是填充里面的属性,没有其他的逻辑,这种情况我们可以偷点懒直接使用 wire.Struct
方法直接生成 provider
// structType: 结构体类型
// fieldNames: 需要填充的字段,使用 "*" 表示所有字段都需要填充
Struct(structType interface{}, fieldNames ...string)
修改一下 Injector
函数
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
// 这里由于只有一个字段,所以这两种是等价的 wire.Struct(new(PostService), "*"),
wire.Struct(new(PostService), "usecase"), // struct 和接口进行绑定
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
))
}
可以看到生成的代码当中自动就生成了一个结构体并且填充数据了
func GetPostService() (*PostService, func(), error) {
iPostRepo, cleanup, err := NewPostRepo()
if err != nil {
return nil, nil, err
}
postUsecase, cleanup2, err := NewPostUsecase(iPostRepo)
if err != nil {
cleanup()
return nil, nil, err
}
// 注意这里
postService := &PostService{
usecase: postUsecase,
}
return postService, func() {
cleanup2()
cleanup()
}, nil
}
3.3 值绑定
除了依赖某一个类型之外,有时候我们还会依赖一些具体的值,这时候我们就可以使用 wire.Value 或者是 wire.InterfaceValue
,为某个类型绑定具体的值
// wire.Value 为某个类型绑定值,但是不能为接口绑定值
Value(interface{}) ProvidedValue
// wire.InterfaceValue 为接口绑定值
InterfaceValue(typ interface{}, x interface{}) ProvidedValue
修改一下 PostService 使他依赖一个int
和io.Reader
然后为它直接绑定 a=99
io.Reader = os.Stdin
// example.go
type PostService struct {
usecase IPostUsecase
a int
r io.Reader
}
// wire.go
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
wire.Struct(new(PostService), "*"), // struct 属性注入
wire.Value(10), // 值绑定
wire.InterfaceValue(new(io.Reader), os.Stdin), // 接口值绑定
wire.Bind(new(IPostUsecase), new(*PostUsecase)), // 结构体绑定接口
NewPostUsecase,
NewPostRepo,
))
}
可以看到生成的代码当中直接生成了两个全局变量
func GetPostService() (*PostService, func(), error) {
iPostRepo, cleanup, err := NewPostRepo()
if err != nil {
return nil, nil, err
}
postUsecase, cleanup2, err := NewPostUsecase(iPostRepo)
if err != nil {
cleanup()
return nil, nil, err
}
int2 := _wireIntValue // 生成的代码绑定全局变量
reader := _wireFileValue
postService := &PostService{
usecase: postUsecase,
a: int2,
r: reader,
}
return postService, func() {
cleanup2()
cleanup()
}, nil
}
// 注意这里
var (
_wireIntValue = 10
_wireFileValue = os.Stdin
)
3.4 ProviderSet(Provider 集合)
在真实的项目当中依赖往往是一组一组的,就像我们的示例一样,只要依赖 PostService
那么 NewPostUsecase NewPostRepo
这两个就必不可少,所以我们往往会创建一些 ProviderSet
在 Injector
函数中直接依赖ProviderSet
就可以了
// 参数是一些 provider
NewSet(...interface{}) ProviderSet
示例如下所示,生成代码和之前一样就不另外贴了
// example.go
// PostServiceSet PostServiceSet
var PostServiceSet = wire.NewSet(
wire.Struct(new(PostService), "*"),
wire.Value(10),
wire.InterfaceValue(new(io.Reader), os.Stdin),
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
)
// wire.go
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
PostServiceSet,
))
}
4. wire 使用最佳实践
4.1 不要使用默认类型
wire 不支持两个提供两个相同类型的 provider
,所以如果我们使用默认类型如 int string 等,只要有两个依赖就会导致报错,解决方案是使用类型别名。
先来看一个报错的示例
//错误的示例
type PostService struct {
usecase IPostUsecase
a int
b int
r io.Reader
}
可以看到,wire
在构建依赖关系的时候,并不知道int
的值该分配给 a
还是 b
所以就会报错
▶ wire .
wire: /Go-000/Week04/blog/03_wire/01_example/example.go:40:2: provider struct has multiple fields of type int
wire: github.com/mohuishou/go-training/Week04/blog/03_wire/01_example: generate failed
wire: at least one generate failure`
自定义两个类型就好了
// 正确的做法
type A int
type B int
// PostService PostService
type PostService struct {
usecase IPostUsecase
a A
b B
r io.Reader
}
// PostServiceSet PostServiceSet
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,
)
4.2 Option Struct
在实际的业务场景当中我们的NewXXX
函数的参数列表可能会很长,这个时候就可以直接定义一个 Option Struct
然后使用 wire.Strcut
来构建 Option Strcut
的依赖
// PostUsecaseOption PostUsecaseOption
type PostUsecaseOption struct {
a A
b B
repo IPostRepo
}
// NewPostUsecase NewPostUsecase
func NewPostUsecase(opt *PostUsecaseOption) (*PostUsecase, func(), error) {
return &PostUsecase{repo: opt.repo}, nil, nil
}
// PostServiceSet PostServiceSet
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,
)
4.3 项目目录结构
.
├── 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
- 这里的