Skip to main content
  1. internet/

go中的依赖注入

·1242 words·3 mins·

现在的服务代码往往需要使用多种中间件、调用多个服务,这些中间件、服务都需要通过参数的方式注入到构造器中,比如这样:

type Service struct {
  userRepo Repo
  userCache Cache
  companyAPI CompanyAPI
  eventPublisher Publisher
}

func NewService(
  userRepo Repo,
	userCache Cache,
	companyAPI CompanyAPI,
  eventPublisher Publisher,
) *Service {
  return &Service{
    userRepo :userRepo,
    userCache :userCache,
    companyAPI :companyAPI,
    eventPublisher :eventPublisher,
  }
}

想象一下代码中有非常多的Service,那么构造函数的初始化过程会相当痛苦!

方法1:将构造所需组件放入一个容器中 #

最简单的方式就是将所有被需要的组件放到一个组件中,然后在构造函数中从这个组件中获取:

func main() {
	container := di.New()
	container.AddSingleton(keyRepo, func(c di.Container) (any, error) {
		return NewUserRepo(), nil
	})
	container.AddSingleton(keyCache, func(c di.Container) (any, error) {
		return NewUserCache(), nil
	})
	container.AddSingleton(keyCompanyAPI, func(c di.Container) (any, error) {
		return NewCompanyAPI(), nil
	})
	container.AddSingleton(keyPublisher, func(c di.Container) (any, error) {
		return NewPublisher(), nil
	})

	service := NewService(container)
	_ = service
}

func NewService(container di.Container) *Service {
	service := Service{
		userRepo:       container.Get(keyRepo).(Repo),
		userCache:      container.Get(keyCache).(Cache),
		companyAPI:     container.Get(keyCompanyAPI).(CompanyAPI),
		eventPublisher: container.Get(keyPublisher).(Publisher),
	}
	return &service
}

container的代码见github

这种方式简化了构造函数的签名——只需要一个container即可。

不过进一步讲,也可以将container放入context中,然后在需要使用组件时从context中获取。

方法2:dig #

dig是uber开源的一款专门用于项目初始化的框架,它使用反射的方式来将组件的实例注册到容器中,然后在Invode时判断需要的组件,然后从容器中获取对应的实例。

package main

import (
	"fmt"
	"go.uber.org/dig"
)

type Repo interface{}

func NewUserRepo() Repo {
	return 1
}

type Cache interface{}

func NewUserCache() Cache {
	return 2
}

type CompanyAPI interface{}

func NewCompanyAPI() CompanyAPI {
	return 3
}

type Publisher interface{}

func NewPublisher() Publisher {
	return 4
}

type Service struct {
	userRepo       Repo
	userCache      Cache
	companyAPI     CompanyAPI
	eventPublisher Publisher
}

func NewService(
	userRepo Repo,
	userCache Cache,
	companyAPI CompanyAPI,
	eventPublisher Publisher,
) *Service {
	return &Service{
		userRepo:       userRepo,
		userCache:      userCache,
		companyAPI:     companyAPI,
		eventPublisher: eventPublisher,
	}
}

func (svc *Service) DoSomething() {
	if svc.userRepo == nil ||
		svc.userCache == nil ||
		svc.companyAPI == nil ||
		svc.eventPublisher == nil {
		panic("init failed")
	}
	fmt.Println("init success")
}

func main() {
	container := dig.New()
	mustProvide(container, NewUserRepo)
	mustProvide(container, NewUserCache)
	mustProvide(container, NewCompanyAPI)
	mustProvide(container, NewPublisher)
	mustProvide(container, NewService)

	err := container.Invoke(func(svc *Service) {
		svc.DoSomething()
	})
	mustNoErr(err)
}

func mustProvide(container *dig.Container, constructor interface{}) {
	err := container.Provide(constructor)
	mustNoErr(err)
}

func mustNoErr(err error) {
	if err != nil {
		panic(err)
	}
}

dig在获取实例时会通过DAG将实例所依赖的组件依次初始化,因此不必担心组件的注册顺序。

dig广为诟病的就是在编译阶段无法知晓哪些组件遗漏了注册或者注册失败,因为dig是通过反射的方式“临时”注册的,因此这个问题无法解决。

方法3:wire #

wire是google开源的用于解决依赖注入的工具。

不同于dig使用反射,wire使用代码生成的方式来自动生成构造函数的代码,如:

type Service struct {
	userRepo       Repo
	userCache      Cache
	companyAPI     CompanyAPI
	eventPublisher Publisher
}

var svcSet = wire.NewSet(
	NewUserRepo,
	NewUserCache,
	NewCompanyAPI,
	NewPublisher,
)

func NewService(
	userRepo Repo,
	userCache Cache,
	companyAPI CompanyAPI,
	eventPublisher Publisher,
) *Service {
	return &Service{
		userRepo:       userRepo,
		userCache:      userCache,
		companyAPI:     companyAPI,
		eventPublisher: eventPublisher,
	}
}

func InitService() (*Service, error) {
	panic(wire.Build(svcSet, NewService))
}

执行wire gen命令,可以看到自动生成了一个名为wire_gen.go的文件,代码内容为:

// Code generated by Wire. DO NOT EDIT.

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

package main

import (
	"github.com/google/wire"
)

// Injectors from main.go:

func InitService() (*Service, error) {
	repo := NewUserRepo()
	cache := NewUserCache()
	companyAPI := NewCompanyAPI()
	publisher := NewPublisher()
	service := NewService(repo, cache, companyAPI, publisher)
	return service, nil
}

// main.go:

type Service struct {
	userRepo       Repo
	userCache      Cache
	companyAPI     CompanyAPI
	eventPublisher Publisher
}

var svcSet = wire.NewSet(
	NewUserRepo,
	NewUserCache,
	NewCompanyAPI,
	NewPublisher,
)

func NewService(
	userRepo Repo,
	userCache Cache,
	companyAPI CompanyAPI,
	eventPublisher Publisher,
) *Service {
	return &Service{
		userRepo:       userRepo,
		userCache:      userCache,
		companyAPI:     companyAPI,
		eventPublisher: eventPublisher,
	}
}

小结 #

我们通过例子讲解了go中三种实现依赖注入的方式。这里只展示了这三种方式最基本的用法,更丰富的用法还待进一步探索。其中:

  1. 通过key-value的方式将组件注册到容器中是最简单直观的方式。
  2. dig中通过反射的方式实现了“动态”注册。dig不用像方法1中需要手动指定key,因此使用上最简单,缺点就是编译阶段无法检测所依赖的组件是否注册成功(个人认为作为项目初始化的工具,这不是一个严重的缺点)。
  3. wire是一个代码生成工具,用于自动生成构造函数。

示例代码 #

golang-notes/di at master · stong1994/golang-notes (github.com)