一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情。
基于 Golang 的依赖注入框架有很多,google/wire, facebook/inject, uber/dig 等等。其实和 ORM 类似,依赖注入底层也需要大量的反射进行支持。笔者近期在工作中使用了 goioc/di 这个框架,体验还不错,如果你是一个从 Java 转 Golang 的开发者,详细看到 goioc/di 的接口时会有一种亲切感。
今天我们一起看看它如何应用,以及实现的原理。
在开始阅读之前,如果有余力我建议大家还是好好看看 Martin Fowler 大神的这一篇 blog,它会告诉你为什么我们需要去注入依赖,为什么要 Inversion of Control:www.martinfowler.com/articles/in…
goioc/di 上手
Simple and yet powerful Dependency Injection for Go
di 的用法很简单,我们参考官方示例走一遍看看 (如果怕麻烦,也可以直接看源码)
我们想要实现的是一个非常简单的 http 服务,它接受一个 path 参数 city(可以是任何你想查询天气的城市),然后调用 wttr.in/ 拿到城市的天气情况。
- 首先参照下图结构创建目录,
-
controllers :顾名思义,一个具体的 API handler,这里不会做业务逻辑,但是会处理好 http 请求和响应与业务的输入输出之间的结构转换;
-
services : 具体的业务逻辑;
-
init.go : 依赖 goioc/di 进行注入,初始化 container;
-
main.go : 启动server,监听端口。
- 补充业务实现
- services/weather_service.go
package services
import (
"io/ioutil"
"net/http"
)
type WeatherService struct {
}
func (ws *WeatherService) Weather(city string) (*string, error) {
response, err := http.Get("https://wttr.in/" + city)
if err != nil {
return nil, err
}
all, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
weather := string(all)
return &weather, nil
}
可以看到,WeatherService 本身没有任何依赖,提供的 Weather 是个纯业务逻辑方法,接受一个入参,输出结果和 error。
类似 WeatherService 这样没有其他需要注入的依赖的结构,其实才是 inject 体系里面最底层的依赖,先有了这些 bean,才可能去初始化其他对 WeatherService 有依赖的 bean。
- controllers/weather_controller.go
package controllers
import (
"net/http"
"github.com/ag9920/di-demo/services"
)
type WeatherController struct {
// note that injection works even with unexported fields
weatherService *services.WeatherService `di.inject:"weatherService"`
}
func (wc *WeatherController) Weather(w http.ResponseWriter, r *http.Request) {
weather, _ := wc.weatherService.Weather(r.URL.Query().Get("city"))
_, _ = w.Write([]byte(*weather))
}
这里也很好理解,作为 controller,WeatherController 的职责在于转发,参数转换。所以他是需要依赖 services.WeatherService 的。这里也是很常见的用 tag 的方式 di.inject:weatherService" 来声明此处需要注入的 bean 的名称。
- init.go
package main
import (
"reflect"
"github.com/ag9920/di-demo/controllers"
"github.com/ag9920/di-demo/services"
"github.com/goioc/di"
)
func init() {
_, _ = di.RegisterBean("weatherService", reflect.TypeOf((*services.WeatherService)(nil)))
_, _ = di.RegisterBean("weatherController", reflect.TypeOf((*controllers.WeatherController)(nil)))
_ = di.InitializeContainer()
}
这里是完成整体依赖注入初始化的地方,声明了 weatherService 以及 weatherController 两个bean,以及对应的类型,注册到 container 里面,最后 InitializeContainer() 完成初始化。
- main.go
package main
import (
"net/http"
"github.com/ag9920/di-demo/controllers"
"github.com/goioc/di"
)
func main() {
http.HandleFunc("/weather", func(w http.ResponseWriter, r *http.Request) {
di.GetInstance("weatherController").(*controllers.WeatherController).Weather(w, r)
})
_ = http.ListenAndServe(":8080", nil)
}
main 函数启动了 http server,这里可以看到,要获取 controller 实例,是通过 di.GetInstance 实现的,拿到了 interface{},再进行类型转换。
好,代码看完了,我们总结一下目前为止看到的 di 用法:
- 通过
di.inject:"{{ name of bean }}"来声明需要注入的 bean; - 初始化时,通过
di.RegisterBean将各个 bean 注册进来。在所有注册完成后,调用di.InitializeContainer()进行注入; - 当需要获取注入的 bean 时,可以采用
di.GetInstance("{{ name of bean }}")来拿到一个 interface{},再转成实际的类型。
单例 vs 原型
这个部分我们结合源码和 API 文档来看看,依赖注入是怎么做到的。
在依赖注入中通常存在两种模式:
- 单例:直接注入单例对象,只存在一个实例,无论什么时候通过 name 来获取 bean 都会返回这一个;
- 原型:注入一个类型,每次需要的时候新创建一个实例。
goioc/di 也支持了这两个,分别对应 Singleton 和 Prototype,同时还补充了一个 http request 声明周期的 scope:Request:
// Scope is an enum for bean scopes supported in this IoC container.
type Scope string
const (
// Singleton is a scope of bean that exists only in one copy in the container and is created at the init-time.
// If the bean is singleton and implements Close() method, then this method will be called on Close (consumer responsibility to call Close)
Singleton Scope = "singleton"
// Prototype is a scope of bean that can exist in multiple copies in the container and is created on demand.
Prototype Scope = "prototype"
// Request is a scope of bean whose lifecycle is bound to the web request (or more precisely - to the corresponding
// context). If the bean implements Close() method, then this method will be called upon corresponding context's
// cancellation.
Request Scope = "request"
)
同样的,除了我们上一节看到的 RegisterBean 这种给出 type 即可注册的形式,goioc/di 也提供了直接注册单例的 RegisterBeanInstance 方法,此后所有对同名 bean 存在依赖的 bean,都会用到同样的单例。
RegisterBean 由于只是提供了类型,理论上讲支持两种 scope,默认为 Singleton,也可以通过
type SomeBean struct {
Scope Scope `di.scope:"prototype"`
}
这样的注解来声明当前字段为 Protoype 的模式。
而 RegisterBeanInstance 则只提供了 Singleton 这一种模式,作者也提到,如果再去 clone 提供的 instance 来实现 Prototype 的话复杂度会提高,从使用的角度来说没有强诉求,暂时没有提供。
源码解析
这一节我们来看源码,了解一下是怎么实现的。
其实 goioc/di 的原理很简单,是很好的 Golang 反射能力训练的样本。可以想见,对于结构体中哪些成员要注入,注入的模式,都是通过 tag 实现,这就势必要用到 reflect 包的强大功能。类似的,如果是 Prototype 的模式,我们需要基于一个类型来创建一个对象,并进行依赖注入,势必也会频繁反射 reflect.Type。
goioc/di 声明了几个全局的变量,这是依赖注入最终的产物,了解了这些,其实原理就清晰了:
var beans = make(map[string]reflect.Type)
var scopes = make(map[string]Scope)
var singletonInstances = make(map[string]interface{})
var userCreatedInstances = make(map[string]bool)
一句话:di 实现注入的本质就是将每一次 Register 的类型/实例解析好之后,把相关的信息放到这些全局变量中。最后基于这些变量对外提供 GetInstance 接口。
-
beans: 所有 bean 类型的集合, key:bean 的名称,value:注入的类型;
-
scopes: 所有 bean 配置的 scope 集合, key: bean 的名称,value: 设置的 Scope (枚举,上面提到的三种)
-
singletonInstances: 单例beans的集合,同样是名称映射到实例,只不过存的是 interface{};
-
userCreatedInstances: 标记哪些是用户自己传入的单例,所以 value 是个 bool,这里其实是一个 set 的语义。
RegisterBean
我们先来看注入类型的方法 RegisterBean,随后再看注入单例的 RegisterBeanInstance。注意参考补充的中文注释:
// RegisterBean function registers bean by type, the scope of the bean should be defined in the corresponding struct
// using a tag `di.scope` (`Singleton` is used if no scope is explicitly specified). `beanType` should be a reference
// type, e.g.: `reflect.TypeOf((*services.YourService)(nil))`. Return value of `overwritten` is set to `true` if the
// bean with the same `beanID` has been registered already.
func RegisterBean(beanID string, beanType reflect.Type) (overwritten bool, err error) {
// 全局一把锁,防止出现并发,毕竟前面可以看到,声明的全是普通的 map
initializeShutdownLock.Lock()
defer initializeShutdownLock.Unlock()
// 原子CAS,经典操作,避免被重复 initialize,若已经初始化了,就不能再 Register
if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
return false, errors.New("container is already initialized: can't register new bean")
}
// 注册的也必须是个指针类型
if beanType.Kind() != reflect.Ptr {
return false, errors.New("bean type must be a pointer")
}
// 判断之前是否已经注册过了(这里就出现了我们前面说的全局变量 beans 这个 map)
var existingBeanType reflect.Type
var ok bool
if existingBeanType, ok = beans[beanID]; ok {
logrus.WithFields(logrus.Fields{
"id": beanID,
"registered bean": existingBeanType,
"new bean": beanType,
}).Warn(beanAlreadyRegistered)
}
// 根据 beanType 来判断要用哪个 scope
beanScope, err := getScope(beanType)
if err != nil {
return false, err
}
beanTypeElement := beanType.Elem()
// 遍历结构体,找到 di.inject tag 的字段,判断能够去 inject,类型是否支持
for i := 0; i < beanTypeElement.NumField(); i++ {
field := beanTypeElement.Field(i)
if _, ok := field.Tag.Lookup(string(inject)); !ok {
continue
}
if field.Type.Kind() != reflect.Ptr && field.Type.Kind() != reflect.Interface &&
field.Type.Kind() != reflect.Slice && field.Type.Kind() != reflect.Map {
return false, errors.New(unsupportedDependencyType)
}
}
// 校验通过,赋值到全局变量即可(这个地方虽然叫 beanID,语义上是唯一的一个 bean 名称,实际是个 string)
beans[beanID] = beanType
scopes[beanID] = *beanScope
return ok, nil
}
看注释应该比较清晰,这里逻辑并不复杂,除去校验逻辑外,真正实现注册的就是获取对应的 scope,然后赋值给 beans 以及 scopes 这两个 map。
类似的,我们来看一下 RegisterBeanInstance 的实现:
// RegisterBeanInstance function registers bean, provided the pre-created instance of this bean, the scope of such beans
// are always `Singleton`. `beanInstance` can only be a reference or an interface. Return value of `overwritten` is set
// to `true` if the bean with the same `beanID` has been registered already.
func RegisterBeanInstance(beanID string, beanInstance interface{}) (overwritten bool, err error) {
initializeShutdownLock.Lock()
defer initializeShutdownLock.Unlock()
if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
return false, errors.New("container is already initialized: can't register new bean")
}
beanType := reflect.TypeOf(beanInstance)
if beanType.Kind() != reflect.Ptr {
return false, errors.New("bean instance must be a pointer")
}
var existingBeanType reflect.Type
var ok bool
if existingBeanType, ok = beans[beanID]; ok {
logrus.WithFields(logrus.Fields{
"id": beanID,
"registered bean": existingBeanType,
"new bean instance": beanType,
}).Warn(beanAlreadyRegistered)
}
beans[beanID] = beanType
scopes[beanID] = Singleton
singletonInstances[beanID] = beanInstance
userCreatedInstances[beanID] = true
return ok, nil
}
你会发现,几乎一模一样,仅有的差异在于倒数第二和倒数第三行:
singletonInstances[beanID] = beanInstance
userCreatedInstances[beanID] = true
除了赋值给 beans 以及 scopes。对于注册单例的场景,会额外将 instance 放到 singletonInstances 这个全局 map 中,并且在 userCreatedInstances 中声明一下,这个是用户指定的单例。
InitializeContainer
初始化 container 的函数 InitializeContainer 逻辑同样不复杂,比较诧异的是,看完全局居然没有一个 默认 container 对象的存在,goioc/di 是直接完全依赖几个全局 map 来提供能力。
下面这部分代码参考中文注释看一下:
// InitializeContainer function initializes the IoC container.
func InitializeContainer() error {
// 跟 Register 同样的一把锁
initializeShutdownLock.Lock()
defer initializeShutdownLock.Unlock()
// 同样,如果已经初始化了,这时候不应该继续处理
if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
return errors.New("container is already initialized: reinitialization is not supported")
}
// 针对 beans 中的每一个 bean type,都创建一个对应的 instance(如果已经有了,会复用)
err := createSingletonInstances()
if err != nil {
return err
}
// 针对每一个创建的 instance 注入依赖
err = injectSingletonDependencies()
if err != nil {
return err
}
// 原子操作,表明已经初始化完成
atomic.StoreInt32(&containerInitialized, 1)
// 针对 bean 做一些后续的业务初始化操作
err = initializeSingletonInstances()
if err != nil {
return err
}
return nil
}
InitializeContainer 这一层的逻辑是很清晰的,真正起作用的就是 createSingletonInstances 以及 injectSingletonDependencies 两个。前者看看还有哪些 bean 是没有创建实例的,如果已经有了就忽略,如果没有,就利用反射创建出来一个对象:
reflect.New(beans[beanID].Elem()).Interface()
注意,此处 beans[beanID] 本质上就是拿这个 bean 的类型。
核心逻辑在于 injectSingletonDependencies,我们来看看。
func injectSingletonDependencies() error {
// 遍历所有创建出来的单例,调用 injectDependencies 实现注入
for beanID, instance := range singletonInstances {
if _, ok := userCreatedInstances[beanID]; ok {
continue
}
if _, ok := beanFactories[beanID]; ok {
continue
}
err := injectDependencies(beanID, instance, make(map[string]bool))
if err != nil {
return err
}
}
return nil
}
为了简化理解,我把switch 里面的具体处理略过了,我们先把握主线。
func injectDependencies(beanID string, instance interface{}, chain map[string]bool) error {
// 先拿到 bean 的 type
instanceType := beans[beanID]
instanceElement := instanceType.Elem()
// 针对每个 bean type,遍历结构体字段,挨个处理注入
for i := 0; i < instanceElement.NumField(); i++ {
// 拿到 field
field := instanceElement.Field(i)
// 如果没 inject 的 tag 就忽略
beanToInject, ok := field.Tag.Lookup(string(inject))
if !ok {
continue
}
// 如果 optional tag 声明不是 bool 就报错,不合规范
optionalDependency, err := isOptional(field)
if err != nil {
return err
}
// 拿到实例,找到对应的 field,利用反射,将要注入的 field 转化为 reflect.Value
fieldToInject := reflect.ValueOf(instance).Elem().Field(i)
fieldToInject = reflect.NewAt(fieldToInject.Type(), unsafe.Pointer(fieldToInject.UnsafeAddr())).Elem()
// 根据具体的 field 类型,进行注入,这里省略处理的部分
// 最后都会体现在 fieldToInject.Set(xxxxx)
switch fieldToInject.Kind() {
case reflect.Ptr, reflect.Interface:
// ...
case reflect.Slice:
// ...
case reflect.Map:
// ...
default:
return errors.New(unsupportedDependencyType)
}
}
return nil
}
可以看到,这里的精华在于,要注入,你首先要拿到那个字段的控制权,从 instance 找到字段,然后转成 reflect.Value,从已有的 instance 里面找到所需要的 bean 之后,通过一个 Set 就能更新进来。
对反射不熟练的同学可以仔细品味一下这里的处理,尤其是用 reflect.NewAt 拿到指针的这一行:
fieldToInject := reflect.ValueOf(instance).Elem().Field(i)
fieldToInject = reflect.NewAt(fieldToInject.Type(), unsafe.Pointer(fieldToInject.UnsafeAddr())).Elem()
fieldToInject.Set(reflect.ValueOf(instanceToInject))
switch 中省略的部分,我们拿出来 Ptr 以及 Interface 这个分支来看一看,这里比较本质,slice 和 map 更多的是兼容逻辑。
switch fieldToInject.Kind() {
case reflect.Ptr, reflect.Interface:
if beanToInject == "" { // injecting by type, gotta find the candidate first
candidates := findInjectionCandidates(fieldToInject.Type())
if len(candidates) < 1 {
if optionalDependency {
continue
} else {
return errors.New("no candidates found for the injection")
}
}
if len(candidates) > 1 {
return errors.New("more then one candidate found for the injection")
}
beanToInject = candidates[0]
}
beanToInjectType := beans[beanToInject]
logInjection(beanID, instanceElement, beanToInject, beanToInjectType)
beanScope, beanFound := scopes[beanToInject]
if !beanFound {
if optionalDependency {
logrus.Trace("no dependency found, injecting nil since the dependency marked as optional")
continue
} else {
return errors.New("no dependency found")
}
}
if beanScope == Request {
return errors.New(requestScopedBeansCantBeInjected)
}
instanceToInject, err := getInstance(context.Background(), beanToInject, chain)
if err != nil {
return err
}
fieldToInject.Set(reflect.ValueOf(instanceToInject))
我们可以看到,上来先判断 beanToInject 是否为空,这个就是从 inject tag 拿到的值。意味着什么呢?
你可以像我们此前这样,明确想要的 bean 是什么:
type WeatherController struct {
// note that injection works even with unexported fields
weatherService *services.WeatherService `di.inject:"weatherService"`
}
其实,也可以这样:
type WeatherController struct {
// note that injection works even with unexported fields
weatherService *services.WeatherService `di.inject:""`
}
不显式地提供 bean 的名称,而是让 goioc/di 帮你寻找一个实现。这就是 inject by type。
后面 getInstance 的部分相对简单了,就是从已经生成的 singletonInstances 里寻找合适的 bean 返回即可(下面一节会介绍)。最后调用 Set 写入:
instanceToInject, err := getInstance(context.Background(), beanToInject, chain)
if err != nil {
return err
}
fieldToInject.Set(reflect.ValueOf(instanceToInject))
GetInstance
好了,上面我们看懂了是怎么 Register,以及怎么初始化完成注入的。其实可以看出来 goioc/di 整体代码还是很清晰的,阅读比较顺畅。最后一步,我们来看看 GetInstance 是怎么实现的。
// GetInstance function returns bean instance by its ID. It may panic, so if receiving the error in return is preferred,
// consider using `GetInstanceSafe`.
func GetInstance(beanID string) interface{} {
beanInstance, err := GetInstanceSafe(beanID)
if err != nil {
panic(err)
}
return beanInstance
}
// GetInstanceSafe function returns bean instance by its ID. It doesnt panic upon explicit error, but returns the error
// instead.
func GetInstanceSafe(beanID string) (interface{}, error) {
if atomic.CompareAndSwapInt32(&containerInitialized, 0, 0) {
return nil, errors.New("container is not initialized: can't lookup instances of beans yet")
}
if scopes[beanID] == Request {
return nil, errors.New("request-scoped beans can't be retrieved directly from the container: they can only be retrieved from the web-context")
}
return getInstance(context.Background(), beanID, make(map[string]bool))
}
鉴于我们目前还不关心 Request 这种 scope,可以看到,除了校验逻辑,本质上底层是直接调用了 getInstance 函数,上面两个都是空壳子。
下面来了,核心部分,参考中文注释理解:
func getInstance(ctx context.Context, beanID string, chain map[string]bool) (interface{}, error) {
// 必须注册过类型
if !isBeanRegistered(beanID) {
return nil, errors.New("bean is not registered: " + beanID)
}
// 如果是默认的单例模式,直接就返回了 全局 singletonInstances 里存的对应实例
if scopes[beanID] == Singleton {
return singletonInstances[beanID], nil
}
// 从这里开始,都是支持 Prototype 以及 Request 两种scope的
// 由于存在嵌套,一定需要检查是不是循环依赖了
if _, ok := chain[beanID]; ok {
return nil, errors.New("circular dependency detected for bean: " + beanID)
}
chain[beanID] = true
// 查询时创建实例
instance, err := createInstance(ctx, beanID)
if err != nil {
return nil, err
}
if _, ok := beanFactories[beanID]; !ok {
err := injectDependencies(beanID, instance, chain)
if err != nil {
return nil, err
}
}
// 这里是执行实例创建后的初始化操作,postConstruct
// 对应于 singleton 里的初始化操作,是在 InitializeContainer 的时候做的,可以往上翻翻
err = initializeInstance(beanID, instance)
if err != nil {
return nil, err
}
err = setContext(ctx, beanID, instance)
if err != nil {
return nil, err
}
return instance, nil
}
这里有一点比较有意思,有时候我们会希望在一个 bean 可用之后进行一些额外的业务操作,goioc/di 提供的能力在这里:
// InitializingBean is an interface marking beans that need to be additionally initialized after the container is ready.
type InitializingBean interface {
// PostConstruct method will be called on a bean after the container is initialized.
PostConstruct() error
}
一个 PostConstruct() 方法,允许业务自己定义在 bean 上面,在上面的 initializeInstance 的时候调用。当然,在 singleton 模式下,由于 bean 本身是在初始化 container 的环节创建的,对应的 initializeInstance 时机也会不同。我们来看下这个函数干了什么:
func initializeInstance(beanID string, instance interface{}) error {
initializingBean := reflect.TypeOf((*InitializingBean)(nil)).Elem()
bean := reflect.TypeOf(instance)
if bean.Implements(initializingBean) {
initializingMethod, ok := bean.MethodByName(initializingBean.Method(0).Name)
if !ok {
return errors.New("unexpected behavior: can't find method PostConstruct() in bean " + bean.String())
}
logrus.WithField("beanID", beanID).Trace("initializing bean")
errorValue := initializingMethod.Func.Call([]reflect.Value{reflect.ValueOf(instance)})[0]
if !errorValue.IsNil() {
return errorValue.Elem().Interface().(error)
}
}
if postprocessors, ok := beanPostprocessors[bean]; ok {
logrus.WithField("beanID", beanID).Trace("postprocessing bean")
for _, postprocessor := range postprocessors {
if err := postprocessor(instance); err != nil {
return err
}
}
}
return nil
}
非常巧妙,怎样在运行时检验某个 struct 是否实现了 某个 interface 呢?
这里前两行就是示例代码:
initializingBean := reflect.TypeOf((*InitializingBean)(nil)).Elem()
bean := reflect.TypeOf(instance)
if bean.Implements(initializingBean) {}
拿到一个空的接口的 reflect.Type,然后拿到实例的 Type,直接看 Implements 的结果。
除此之外,goioc/di 还提供了 postprocessor,供开发者进一步扩展你想进行的操作,提供形如func(bean interface{}) error 的函数,注册进来即可。
// RegisterBeanPostprocessor function registers postprocessors for beans. Postprocessor is a function that can perform
// some actions on beans after their creation by the container (and self-initialization with PostConstruct).
func RegisterBeanPostprocessor(beanType reflect.Type, postprocessor func(bean interface{}) error) error {
initializeShutdownLock.Lock()
defer initializeShutdownLock.Unlock()
if atomic.CompareAndSwapInt32(&containerInitialized, 1, 1) {
return errors.New("container is already initialized: can't register bean postprocessor")
}
beanPostprocessors[beanType] = append(beanPostprocessors[beanType], postprocessor)
return nil
}
总体来说,goioc/di 的代码还是非常整洁,阅读性很高的,建议大家有时间的话完整地过一遍源码。 在 container 初始化后进行后续扩展的方面,也提供了 PostContruct 以及 PostProcessor 两层能力。