Go的依赖注入库dig

Posted F3nGaoXS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go的依赖注入库dig相关的知识,希望对你有一定的参考价值。

什么是dig?

dig是uber的开源的实现了依赖注入的一个库。如果你熟悉Java的话,我相信你对大名鼎鼎的Spring以及SpringIoC一定会有所了解,SpringIoC就是Java的依赖注入的实现。而dig则是golang的依赖注入的实现,不过dig很小巧且简洁,只不过易用性相较于SpringIoC会差一点。

第一个dig应用

  1. 从配置文件中读取配置信息
  2. 利用读取到的配置信息初始化简单的App对象
  3. 最后对User对象进行打印

安装库

由于需要读取配置文件,所以我们需要用到godotenv库,如果你不会使用它,没关系,它很简单。

$ go get go.uber.org/dig
$ go get github.com/joho/godotenv

具体实现逻辑

.env文件:

app_name = test-demo
app_creator = FanGaoXS
app_version = 0.0.1

.env中读取配置信息:

type EnvOption struct 
	AppName    string
	AppCreator string
	AppVersion string


func InitEnv() (*EnvOption, error) 
  // 从根目录下的.env文件中读取配置文件(以key-value的形式)
  // 返回key-value的map
	envMap, err := godotenv.Read(".env")
	if err != nil 
		return nil, err
	

	return &EnvOption
		AppName:    envMap["app_name"],
		AppCreator: envMap["app_creator"],
		AppVersion: envMap["app_version"],
	, nil

该func的不需要其他依赖,并且返回值是EnvOption实例。

构建App对象:

type App struct 
	Name    string
	Version string


func InitApp(opt *EnvOption) *App 
	return &App
		Name:    opt.AppName,
		Version: opt.AppVersion,
	

该func需要EnvOption实例,返回App实例。

最后对App对象进行打印:

func printApp(app *App) 
	fmt.Printf("app = %#v\\n", app)

该func需要App实例,没有返回。

自动依赖注入

func main() 
	// 1, 创建容器
	container := dig.New()

	// 2, 将对象的构造函数provide
	container.Provide(InitEnv)
	container.Provide(InitApp)

	// 3, 将函数需要的依赖从容器中注入
	container.Invoke(printApp)

依赖关系

我们可以简单来理一下这个依赖关系:

printApp依赖于InitApp,InitApp依赖于InitEnv

传统依赖关系

使用依赖注入容器思想的依赖关系

可以看到,传统的依赖关系是依赖于具体某个函数或者及其返回值。但是使用依赖注入思想的依赖关系,则是将依赖放入容器当中,然后需要某个依赖直接从容器中取用。这样不仅可以解决对象可能会复用的问题,还可以解决复杂的依赖链问题。(注意,此处所指的依赖注入思想不局限于dig库,适用于所有实现了依赖注入思想的库)。

完整代码

package main

import (
	"fmt"
	"github.com/jessevdk/go-flags"
	"github.com/joho/godotenv"
	"go.uber.org/dig"
)

type EnvOption struct 
	AppName    string
	AppCreator string
	AppVersion string


func InitEnv() (*EnvOption, error) 
	envMap, err := godotenv.Read(".env")
	if err != nil 
		return nil, err
	

	return &EnvOption
		AppName:    envMap["app_name"],
		AppCreator: envMap["app_creator"],
		AppVersion: envMap["app_version"],
	, nil


type App struct 
	Name    string
	Version string


func InitApp(opt *EnvOption) *App 
	return &App
		Name:    opt.AppName,
		Version: opt.AppVersion,
	


func printApp(app *App) 
	fmt.Printf("app = %#v\\n", app)


func main() 
	// 1, 创建容器
	container := dig.New()

	// 2, 将对象的构造函数provide
	container.Provide(InitEnv)
	container.Provide(InitApp)

	// 3, 将函数需要的依赖从容器中注入
	container.Invoke(printApp)

Tips

需要特别注意的是,即使是provide简单的对象,也不能直接provide对象的地址,而是利用使用函数返回对象,然后provide该函数。

e.g:

		// 错误示范:
		u = NewUser()
		container.Provide(u) <=> container.Provider(NewUser()) :

 	  // 正确示范:
		func initUser() *Userreturn NewUser()
		container.Provide(initUser)

参数对象

有的时候,如果某个func需要多个依赖,像这样:

container.Provide(func (arg1 *Arg1, arg2 *Arg2, arg3 *Arg3, ....) 
  // ...
)

我们可以利用dig.In将它们合并起来:

type Args 
  dig.In

  Arg1 *Arg1
  Arg2 *Arg2
  Arg3 *Arg3
  Arg4 *Arg4

container.Provide(func (args Args) *Object 
  // ...
)

示例

  1. 依旧是从.env中读User对象和Shoe对象的配置信息,构建Option实例并且返回
  2. 分别利用Option初始化User和Shoe对象
  3. 打印User和Shoe对象
type UserEnv struct 
	Name string
	Age  int


type ShoeEnv struct 
	Brand string


type Option struct 
	User *UserEnv
	Shoe *ShoeEnv


func InitEnv() *Option 
	envMap, err := godotenv.Read(".env")
	if err != nil 
		log.Fatalln("read env err")
	

	age, _ := strconv.Atoi(envMap["user_age"])

	return &Option
		User: &UserEnv
			Name: envMap["user_name"],
			Age:  age,
		,
		Shoe: &ShoeEnv
			Brand: envMap["shoe_brand"],
		,
	


func initUser(opt *env.Option) *model.User 
	return model.NewUser(opt.User.Name, opt.User.Age)


func initShoe(opt *env.Option) *model.Shoe 
	return model.NewShoe(opt.Shoe.Brand)


// Args 在invoke的时候,方便直接使用Args结构体对象的属性来进行调用
// 如arg.User来使用容器中的user实例
type Args struct 
	dig.In

	User *model.User
	Shoe *model.Shoe


// 使用dig.In
func print(args Args) 
	fmt.Printf("user = %#v\\n", args.User)
	fmt.Printf("shoe = %#v\\n", args.Shoe)


func main() 
	container := dig.New()

	container.Provide(env.InitEnv)
	container.Provide(initUser)
	container.Provide(initShoe)

	container.Invoke(print)


可以看到,最后打印的时候,直接使用了args实例里的User对象和Shoe对象。

等价于:

type Args struct 
	User *model.User
	Shoe *model.Shoe


// 使用dig.In
func print(user *model.User,shoe *model.Shoe) 
	fmt.Printf("user = %#v\\n", user)
	fmt.Printf("shoe = %#v\\n", shoe)

结果对象

类似的,如果一个函数返回多个结果,像这样:

container.Provide(func () (result1, result2, result4, result4, error) 
  // ...
)

可以使用dig.Out将它们合并起来:

type Results struct 
  dig.Out

  Result1 *Result1
  Result2 *Result2
  Result3 *Result3
  Result4 *Result4


container.Provide(func () (Results, error)
  // ...
)

示例

将上一个节的思想稍作修改:

  1. .env文件中读取user和shoe的配置信息,并且构建option实例
  2. 利用option构建user和shoe
  3. 打印user和shoe
type Result struct 
	dig.Out

	User *model.User
	Shoe *model.Shoe


// 使用dig.out的时候,返回值直接返回包含dig.out的结构体实例
func initUserAndShoe(opt *env.Option) Result 
	return Result
		User: &model.User
			Name: opt.User.Name,
			Age:  opt.User.Age,
		,
		Shoe: &model.Shoe
			Brand: opt.Shoe.Brand,
		,
	


// 使用dig.out:直接使用具体的对象实例,而不是Result实例的属性:
// 如直接使用User实例,而不是Result.User实例
func printInfo(user *model.User, shoe *model.Shoe) 
	fmt.Printf("user = %#v\\n", user)
	fmt.Printf("shoe = %#v\\n", shoe)


func main() 
	container := dig.New()

	container.Provide(env.InitEnv)
	container.Provide(initUserAndShoe)

	err := container.Invoke(printInfo)
	if err != nil 
		log.Fatal(err)
	

可以看到在初始化User和Shoe对象时,将user和shoe实例合并返回了。但是在使用的时候,则是分别使用User和Shoe对象。

命名

有的时候我们可能需要同一结构体的不同实例,如使用不同的user实例,我们可以利用dig.Name来分别将它们命名,然后再使用指定的实例就可以了。

示例

// 构造函数不能返回结构体对象,应当返回函数
func initUser(name string, age int) func() *model.User 
	return func() *model.User 
		return &model.UserName: name, Age: age
	


type UsersInArg struct 
	dig.In

	// 指定名称来使用
	U1 *model.User `name:"u1"`
	U2 *model.User `name:"u2"`


func PrintUsers(arg UsersInArg) 
	fmt.Printf("user1 = %#v\\n", arg.U1)
	fmt.Printf("user2 = %#v\\n", arg.U2)


func main() 
	container := dig.New()

	// 将同一结构体的不同对象放入容器当中,需要命名,并且再取用的时候需要利用dig.in并且指定名称来使用
	container.Provide(initUser("t1", 18), dig.Name("u1"))
	container.Provide(initUser("t2", 20), dig.Name("u2"))

	container.Invoke(PrintUsers)

provide的时候将两个不同的user实例放入了容器,并且为它们命了名,这样在使用的时候就可以结合dig.In和tag:name:"xx"来使用。

分组

和上述情况类似的,如果将同一结构体的多个不同对象放入容器中,但是并不需要指定某个名称来使用,就可以利用dig.Group将它们分组。

示例

func initUser(name string, age int) func() *model.User 
	return func() *model.User 
		return &model.UserName: name, Age: age
	


type Args struct 
	dig.In

	Users []*model.User `group:"user"`


func printInfo(args Args) 
	for i, u := range args.Users 
		fmt.Printf("user[%d] = %#v\\n", i, u)
	


func main() 
	container := dig.New()

	container.Provide(initUser("u1", 18), dig.Group("user"))
	container.Provide(initUser("u2", 18), dig.Group("user"))

	container.Invoke(printInfo)

provide的时候将多个user实例放入了容器,并且使用dig.Group将它们分了组,最后使用dig.In和tag:group:"user"来访问该实例列表。由于是将实例分组,所以并不保证访问实例的顺序

常见错误

在使用invoke的时候常常会返回一些错误

  • 无法找到依赖,或依赖创建失败;
  • Invoke执行的函数返回error,该错误也会被传给调用者。

这两种情况,我们都可以判断Invoke的返回值来查找原因。

参考

  1. dig GitHub:https://github.com/uber-go/dig
  2. darjun每日一库:https://darjun.github.io/2020/02/22/godailylib/dig/

以上是关于Go的依赖注入库dig的主要内容,如果未能解决你的问题,请参考以下文章

go依赖注入dig包使用-来自uber公司

Go 开源库运行时依赖注入框架 Dependency injection

go 依赖注入 简单 例子

清晰架构(Clean Architecture)的Go微服务: 依赖注入(Dependency Injection)

Go team 开源项目 Go Cloud 使用的依赖注入工具 Wire 怎么使用?

wire 源码分析