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

Posted YOYOFx

tags:

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

Dependency injection

一个Go编程语言的运行依赖注入库。依赖注入是更广泛的控制反转技术的一种形式。它用于增加程序的模块化并使其具有可扩展性。

实例展示(High API):

type A struct 
	Name string


func NewA() *A 
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	name := "A-" + strconv.Itoa(r.Int())
	return &AName: ls


services := NewServiceCollection()
services.AddSingleton(NewA)
//serviceCollection.AddSingletonByImplementsAndName("redis-master", NewRedis, new(abstractions.IDataSource))
//serviceCollection.AddTransientByImplements(NewRedisClient, new(redis.IClient))
//serviceCollection.AddTransientByImplements(NewRedisHealthIndicator, new(health.Indicator))
serviceProvider := services.Build()

var env *A
_ = serviceProvider.GetService(&env) // used

依赖注入将如何帮助我们的?

依赖注入是更广泛的控制反转技术的一种形式。它用于增加程序的模块化并使其具有可扩展性。

安装

go get -u github.com/yoyofxteam/dependencyinjection@v1.0.1

组件特性

  • Providing
  • Extraction
  • Invocation
  • Lazy-loading
  • Interfaces
  • Groups
  • Named definitions
  • Optional parameters
  • Parameter Bag
  • Prototypes
  • Cleanup

Providing

首先,我们需要创建两个基本类型:http。服务器和http.ServeMux。让我们创建一个简单的构造函数来初始化它:

// NewServer creates a http server with provided mux as handler.
func NewServer(mux *http.ServeMux) *http.Server 
	return &http.Server
		Handler: mux,
	


// NewServeMux creates a new http serve mux.
func NewServeMux() *http.ServeMux 
	return &http.ServeMux

支持的构造器签名形式如下:

func([dep1, dep2, depN]) (result, [cleanup, error])

现在让我们来容器构建这些类型。

下面展示是Low API, 也可以用High API来构建:

import (
  di "github.com/yoyofxteam/dependencyinjection"
)

container := di.New(
	// provide http server
    di.Provide(NewServer),
    // provide http serve mux
    di.Provide(NewServeMux)
)

函数di. new()解析构造函数,编译依赖关系图并返回*di。用于交互的容器类型。如果无法编译,容器会出现恐慌。

我认为在应用程序初始化时而不是在运行时出现恐慌是很常见的。

Extraction

我们可以从容器中提取构建的服务器。为此,定义提取类型的变量,并将变量指针传递给Extract函数。

如果未找到提取的类型或构建实例的过程导致错误,则提取返回错误。
如果没有错误发生,我们就可以使用这个变量,就像我们自己构建它一样。

// declare type variable
var server *http.Server
// extracting
err := container.Extract(&server)
if err != nil 
	// check extraction error


server.ListenAndServe()

请注意,默认情况下,容器作为单例创建实例 , 但也可使用Provide做行为上的改变。


## Invocation
作为提取的替代方法,我们可以使用Invoke()函数。它解析函数依赖并调用函数。调用函数可能返回可选错误。
```golang
// NewServer creates a http server with provided mux as handler.
func NewServer(handler http.Handler) *http.Server 
	return &http.Server
		Handler: handler,
	

对于一个容器来说,要知道作为http的实现。处理程序是必须使用的,我们使用选项di.As()。这个选项的参数必须是一个指向接口的指针,比如new(Endpoint)。

这种语法可能看起来很奇怪,但我还没有找到更好的方式来指定接口。

修改依赖注入容器初始化代码:

container := inject.New(
	// provide http server
	inject.Provide(NewServer),
	// provide http serve mux as http.Handler interface
	inject.Provide(NewServeMux, inject.As(new(http.Handler)))
)

现在容器使用提供*http。ServeMux作为http。服务器构造函数中的处理程序。使用接口有助于编写更多可测试的代码。

Groups

容器自动将接口的所有实现分组到[]组。例如,提供with inject.As(new(http.Handler))自动创建一个组[]http.Handler。

让我们使用这个特性添加一些http控制器。控制器有典型的行为。它正在注册路由。首先,将为它创建一个接口。

// Controller is an interface that can register its routes.
type Controller interface 
	RegisterRoutes(mux *http.ServeMux)

现在我们将编写控制器并实现控制器接口。

// OrderController is a http controller for orders.
type OrderController struct 

// NewOrderController creates a auth http controller.
func NewOrderController() *OrderController 
	return &OrderController


// RegisterRoutes is a Controller interface implementation.
func (a *OrderController) RegisterRoutes(mux *http.ServeMux) 
	mux.HandleFunc("/orders", a.RetrieveOrders)


// Retrieve loads orders and writes it to the writer.
func (a *OrderController) RetrieveOrders(writer http.ResponseWriter, request *http.Request) 
	// implementation

UserController

// UserController is a http endpoint for a user.
type UserController struct 

// NewUserController creates a user http endpoint.
func NewUserController() *UserController 
	return &UserController


// RegisterRoutes is a Controller interface implementation.
func (e *UserController) RegisterRoutes(mux *http.ServeMux) 
	mux.HandleFunc("/users", e.RetrieveUsers)


// Retrieve loads users and writes it using the writer.
func (e *UserController) RetrieveUsers(writer http.ResponseWriter, request *http.Request) 
    // implementation

就像在这个接口的例子中,我们将使用inject.As()提供provide选项。

container := inject.New(
	di.Provide(NewServer),        // provide http server
	di.Provide(NewServeMux),       // provide http serve mux
	// endpoints
	di.Provide(NewOrderController, di.As(new(Controller))),  // provide order controller
	di.Provide(NewUserController, di.As(new(Controller))),  // provide user controller
)

现在,我们可以在mux中使用控制器数组。参见更新后的代码:

// NewServeMux creates a new http serve mux.
func NewServeMux(controllers []Controller) *http.ServeMux 
	mux := &http.ServeMux

	for _, controller := range controllers 
		controller.RegisterRoutes(mux)
	

	return mux

高级功能

Named definitions

在某些情况下,一种类型有多个实例。例如两个数据库实例:master -用于写,slave -用于读。

// MasterDatabase provide write database access.
type MasterDatabase struct 
	*Database


// SlaveDatabase provide read database access.
type SlaveDatabase struct 
	*Database

第二种方法是使用带有di.WithName()提供选项的命名定义

// provide master database
di.Provide(NewMasterDatabase, di.WithName("master"))
// provide slave database
di.Provide(NewSlaveDatabase, di.WithName("slave"))

如果需要从容器中提取,请使用di.Name()提取选项。

var db *Database
container.Extract(&db, di.Name("master"))

如果需要在其他构造函数中提供命名定义,请使用di。参数嵌入。

// ServiceParameters
type ServiceParameters struct 
	di.Parameter
	
	// use `di` tag for the container to know that field need to be injected.
	MasterDatabase *Database `di:"master"`
	SlaveDatabase *Database  `di:"slave"`


// NewService creates new service with provided parameters.
func NewService(parameters ServiceParameters) *Service 
	return &Service
		MasterDatabase:  parameters.MasterDatabase,
		SlaveDatabase: parameters.SlaveDatabase,
	

Optional parameters

// ServiceParameter
type ServiceParameter struct 
	di.Parameter
	
	StdOutLogger *Logger `di:"stdout"`
	FileLogger   *Logger `di:"file,optional"`

将依赖声明为可选的构造函数必须处理这些依赖不存在的情况。

Parameter Bag

如果需要在定义级别上指定一些参数,则可以使用 inject.ParameterBag provide option. 这是一个map[string]interface,可以转换为di.ParameterBag类型。

// Provide server with parameter bag
di.Provide(NewServer, di.ParameterBag
	"addr": ":8080",
)

// NewServer create a server with provided parameter bag. Note: use di.ParameterBag type.
// Not inject.ParameterBag.
func NewServer(pb di.ParameterBag) *http.Server 
	return &http.Server
		Addr: pb.RequireString("addr"),
	

Prototypes

如果您想在每次提取上创建一个新实例,请使用di.Prototype()提供选项。

di.Provide(NewRequestContext, di.Prototype())

Cleanup

如果提供程序创建了一个需要清理的值,那么它可以返回一个闭包来清理资源。

func NewFile(log Logger, path Path) (*os.File, func(), error) 
    f, err := os.Open(string(path))
    if err != nil 
        return nil, nil, err
    
    cleanup := func() 
        if err := f.Close(); err != nil 
            log.Log(err)
        
    
    return f, cleanup, nil

在调用container.Cleanup()之后,它遍历实例,如果存在则调用cleanup函数。

container := di.New(
	// ...
    di.Provide(NewFile),
)

// do something
container.Cleanup() // file was closed

总结

Dependency injection 是一个运行时的依赖注入框架, 它也会集成到了微服务框架 yoyogo 中.

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 开源库运行时依赖注入框架 Dependency injection的主要内容,如果未能解决你的问题,请参考以下文章

Go的依赖注入库dig

Android 常用开源框架源码解析 系列 dagger2 呆哥兔 依赖注入库

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

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

Android ButterKnife依赖注入框架的引入和使用

go 依赖注入 简单 例子