0%

Go 依赖注入

Go 依赖注入

软件工程中,依赖注入(dependency injection,缩写为 DI)是一种软件设计模式,也是实现控制反转的其中一种技术。这种模式能让一个物件接收它所依赖的其他物件。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”[1]。此模式确保了任何想要使用给定服务的物件不需要知道如何建立这些服务。取而代之的是,连接收方物件(像是 client)也不知道它存在的外部代码(注入器)提供接收方所需的服务。

该设计的目的是为了分离关注点,分离接收方和依赖,从而提供松耦合以及代码重用性

from wiki

以上wiki是从概念上来解释Dependency Injection,对于初学者一看有点晕,其实道理很简单,一个函数需要传入各种参数来实例化一个对象,现在使用Dependency Injection方式的话,直接传入该对象作为参数。这就是所谓的『依赖是注入进来的』,而和它的构造方式解耦了。构造和销毁这些『控制』操作也交给了第三方,也就是『控制反转』。

在 Go 中,这通常采用将依赖项传递给构造函数的形式:

1
2
// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

使用go进行业务开发,刚开始的时候项目通常依赖不多,随着需求以及开发人员越来越多之后,项目依赖也随着增加,而且依赖之间还有一定的先后依赖关系,甚至还有一些隐式的依赖关系。如果有趁手的依赖管理框架的话,可以大大减少开发人员的心智负担。

golang依赖注入框架有两个派别,一种是通过反射在运行时进行依赖注入,代表是Ubser的dig开源项目,另一种是进行代码生成在编译的时候完成注入,代表是google的wire项目。使用 dig 功能会强大一些,但是缺点就是错误只能在运行时才能发现,使用 wire 的缺点就是功能限制多一些,但是好处就是编译的时候就可以发现问题,wire使用者相对会多点,本文主要阐述的也是基于wire的依赖管理。

What is Wire

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

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

1
func NewCartUseCase(repo CartRepo, logger log.Logger) *CartUseCase

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

1
2
3
4
5
// +build wireinject

func GetCartService() *Blog {
panic(wire.Build(NewCartService, NewCartUsecase, NewCartRepo))
}

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

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

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
31
32
33
34
35
36
37
package example

// simple wire example, not used in production

// ICartRepo ICartRepo
type ICartRepo interface{}

// NewCartRepo NewCartRepo
func NewCartRepo() ICartRepo {
return new(ICartRepo)
}

// usecase

// ICartUsecase ICartUsecase
type ICartUsecase interface{}

type cartUsecase struct {
repo ICartRepo
}

// NewCartUsecase NewCartUsecase
func NewCartUsecase(repo ICartRepo) ICartUsecase {
return cartUsecase{repo: repo}
}

// service service

// CartService CartService
type CartService struct {
usecase ICartUsecase
}

// NewCartService NewCartService
func NewCartService(u ICartUsecase) *CartService {
return &CartService{usecase: u}
}

wire.go的文件内容

1
2
3
4
5
6
7
8
9
10
11
package example

import "github.com/google/wire"

func GetCartService() *CartService {
panic(wire.Build(
NewCartService,
NewCartUsecase,
NewCartRepo,
))
}

执行wire . 之后可以生成的函数和我们自己手写其实差不多

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

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

package example

// Injectors from wire.go:

func GetCartService() *CartService {
iCartRepo := NewCartRepo()
iCartUsecase := NewCartUsecase(iCartRepo)
cartService := NewCartService(iCartUsecase)
return cartService
}

How wire works

Wire 主要受 Java 的Dagger 2启发,使用代码生成而不是反射或Service Loader,主要有如下几个优点:

  • 方便 debug,若有依赖缺失编译时会报错
  • 因为不需要 Service Locators, 所以对命名没有特殊要求
  • 避免依赖膨胀。生成的代码只包含被依赖的代码,而运行时依赖注入则无法作到这一点
  • 依赖关系静态存于源码之中, 便于工具分析与可视化

参考资料:

  1. GitHub - uber-go/dig: A reflection based dependency injection toolkit for Go
  2. GitHub - google/wire: Compile-time Dependency Injection for Go
  3. https://medium.com/@dche423/master-wire-cn-d57de86caa1b
  4. https://go.dev/blog/wire