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
容器自动将接口的所有实现分组到[]
让我们使用这个特性添加一些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应用
- 从配置文件中读取配置信息
- 利用读取到的配置信息初始化简单的App对象
- 最后对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
// ...
)
示例
- 依旧是从.env中读User对象和Shoe对象的配置信息,构建Option实例并且返回
- 分别利用Option初始化User和Shoe对象
- 打印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)
// ...
)
示例
将上一个节的思想稍作修改:
- 从
.env
文件中读取user和shoe的配置信息,并且构建option实例 - 利用option构建user和shoe
- 打印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
的返回值来查找原因。
参考
- dig GitHub:https://github.com/uber-go/dig
- darjun每日一库:https://darjun.github.io/2020/02/22/godailylib/dig/
以上是关于Go 开源库运行时依赖注入框架 Dependency injection的主要内容,如果未能解决你的问题,请参考以下文章
Android 常用开源框架源码解析 系列 dagger2 呆哥兔 依赖注入库
清晰架构(Clean Architecture)的Go微服务: 依赖注入(Dependency Injection)
Go team 开源项目 Go Cloud 使用的依赖注入工具 Wire 怎么使用?