go dig学习笔记,依赖注入,反转控制

作者: adm 分类: go 发布时间: 2024-05-13

IOC、DI介绍
IOC、DI这两词相信对于过去接触过大名鼎鼎的Spring的小伙伴来说应该并不陌生,但很多人往往不能第一时间说出这两个词的主要意思,然后对于一些没有Spring使用经验的人来说,可能就是显得比较陌生。这里简单的介绍一下这两个词的意思。

IOC(Inversion of Control):中文为 “反转控制” ,是一种将设计好的对象交给第三方(IOC容器)来控制设计思想。不同于传统的由程序员来控制对象生命周期的方式,而是由IOC容器来完成对象生命周期的控制。

DI(Dependency Injection):中文为 “依赖注入” ,在创建某一对象时,可能会依赖一些其它的对象,传统方式一般是先创建出依赖对象,然后才能创建出目标对象。当使用了IOC容器后,则当我们获取目标对象时,IOC容器会为我们自动注入依赖对象,不再需要我们手动去创建。

2. Dig介绍
Dig 是Uber开源的一个golang的轻量Ioc库,其主要的实现技术为反射,相对于wire和go-Spring比较易用。

3. Dig使用
注:以下内容搬运于,感兴趣的小伙伴可以直接访问原文。

3.1 创建容器
使用以下方法创建一个dig容器实例。

c := dig.New()

3.2 Provide
Provide方法用于注册对象的构造方法,传入参数是一个函数,这个函数的形参列表就是需要dig容器注入的对象(依赖对象)。dig容器会根据传入的函数,在需要时为我们创建出函数的返回值对象(目标对象),并讲返回值放入dig容器中。

err := c.Provide(func(conn *sql.DB) (*UserGateway, error) {
  // ...
})
if err != nil {
  // ...
}

if err := c.Provide(newDBConnection); err != nil {
  // ...
}
对于每一个构造方法的返回值,dig只会创建出一个实例(单例模式)。
err := c.Provide(func(conn *sql.DB) *CommentGateway {
  // ...
})
if err != nil {
  // ...
}

构造方法可以定义任意个形参,并且可以返回错误。

err := c.Provide(func(u *UserGateway, c *CommentGateway) (*RequestHandler, error) {
  // ...
})
if err != nil {
  // ...
}

if err := c.Provide(newHTTPServer); err != nil {
  // ...
}

构造方法也可以定义任意个返回值,并将这些返回值加入到dig容器中。

err := c.Provide(func(conn *sql.DB) (*UserGateway, *CommentGateway, error) {
  // ...
})
if err != nil {
  // ...
}

Provide方法会忽略掉可变参数的形参。下面这段代码,

func NewVoteGateway(db *sql.DB, options ...Option) *VoteGateway

相当于

func NewVoteGateway(db *sql.DB) *VoteGateway
3.3 Invoke
使用Invoke方法来使用添加到容器的类型Invoke的参数是一个函数,这个函数的形参可以是一个或多个,并且可以选择返回错误。dig容器会实例话这个方法形参列表中的实例,如果容器中没有管理形参中对象的构造方法或实例的话则会调用失败。

err := c.Invoke(func(l *log.Logger) {
  // ...
})
if err != nil {
  // ...
}

err := c.Invoke(func(server *http.Server) error {
  // ...
})
if err != nil {
  // ...
}

在传入函数返回的错误会返回给Invoke方法的调用者。

3.4 Parameter Objects(参数对象)
当构造方法的形参列表比较长时,也就是依赖的对象比较多时,会导致代码可读性变差。

func NewHandler(users *UserGateway, comments *CommentGateway, posts *PostGateway, votes *VoteGateway, authz *AuthZGateway) *Handler {
  // ...
}

这时我们可以建一个参数对象,并将方法的形参列表替换成这个参数对象来优化代码的可读性。参数对象需要内嵌dig.In结构体,如下所示:

type HandlerParams struct {
  dig.In

  Users    *UserGateway
  Comments *CommentGateway
  Posts    *PostGateway
  Votes    *VoteGateway
  AuthZ    *AuthZGateway
}

func NewHandler(p HandlerParams) *Handler {
  // ...
}

dig可以处理参数对象和参数的各种组合。

func NewHandler(p HandlerParams, l *log.Logger) *Handler {
  // ...
}

3.5 Result objects(返回对象)
当传入函数的返回值的数量太多,也一样会破坏代码的可读性。

func SetupGateways(conn *sql.DB) (*UserGateway, *CommentGateway, *PostGateway, error) {
  // ...
}

这时可以在一个新的结构体中内嵌dig.Out,来构造一个返回对象,用于替换这些过多的返回值。

type Gateways struct {
  dig.Out

  Users    *UserGateway
  Comments *CommentGateway
  Posts    *PostGateway
}

func SetupGateways(conn *sql.DB) (Gateways, error) {
  // ...
}

3.6 Optional Dependencies(可选依赖)
一些情况下,目标对象依赖的一些对象,可能是可有可无的,就算没有这些依赖对象,目前对象也能以一种服务降级的状态运行。可以在内嵌了dig.In结构体中,在可选依赖字段后面增加optional:"true"的tag。

type UserGatewayParams struct {
  dig.In

  Conn  *sql.DB
  Cache *redis.Client `optional:"true"`
}
当可选依赖不可用时,dig会将这个字段置为0值。

func NewUserGateway(p UserGatewayParams, log *log.Logger) (*UserGateway, error) {
  if p.Cache == nil {
    log.Print("Logging disabled")
  }
  // ...
}

注意:使用了可选参数必须在目标对象的结构体方法中指定可选参数不存在时的逻辑。

3.7 Named Values(命名值)
一些目标对象会用到相同类型的依赖对象,但由于默认dig对相同对象是只保存一个实例的,这时可以通过命名值来允许添加多个相同类型的对象到dig容器中。

假设我们有下面两个构造方法,他们返回值的类型是一样的。

func NewReadOnlyConnection(...) (*sql.DB, error)
func NewReadWriteConnection(...) (*sql.DB, error)

你可以通过Provide方法的dig.Name选项来给这两个构造方法的返回值命名。

c.Provide(NewReadOnlyConnection, dig.Name("ro"))
c.Provide(NewReadWriteConnection, dig.Name("rw"))

或者在内嵌了Dig.Out的结构体字段后面加上name:".."tag

type ConnectionResult struct {
  dig.Out

  ReadWrite *sql.DB `name:"rw"`
  ReadOnly  *sql.DB `name:"ro"`
}

func ConnectToDatabase(...) (ConnectionResult, error) {
  // ...
  return ConnectionResult{ReadWrite: rw, ReadOnly:  ro}, nil
}

不管是用上面的哪种方式,使用命名值时都需要用一个内嵌了dig.In的结构体中需要注入命名值的字段后面加上name:".."tag,tag的值要跟上面一致来接收注入的引用。

type GatewayParams struct {
  dig.In

  WriteToConn  *sql.DB `name:"rw"`
  ReadFromConn *sql.DB `name:"ro"`
}
这个tag可以跟可选参数的tag组合使用。

type GatewayParams struct {
  dig.In

  WriteToConn  *sql.DB `name:"rw"`
  ReadFromConn *sql.DB `name:"ro" optional:"true"`
}

func NewCommentGateway(p GatewayParams, log *log.Logger) (*CommentGateway, error) {
  if p.ReadFromConn == nil {
    log.Print("Warning: Using RW connection for reads")
    p.ReadFromConn = p.WriteToConn
  }
  // ...
}

3.8 Value Groups(值组)
dig提供了组值功能,用于生产合消费多个相同类型的对象。组值允许构造方法返回一个命名的无序的集合到dig容器中。其他的构造方法可以用切片来请求这个集合的所有值。

在内嵌了dig.Out的结构体中,在需要使用组值功能的字段后面加上group:".."tag。

type HandlerResult struct {
  dig.Out

  Handler Handler `group:"server"`
}

func NewHelloHandler() HandlerResult {
  ..
}

func NewEchoHandler() HandlerResult {
  ..
}

任意个构造函数都可以向这个组中加入值,其他构造函数可以通过使用一个内嵌了dig.In的结构体中一个加了group:".."tag的切片字段来接收这个组的值。这些构造方法并没有特定的执行顺序。

type ServerParams struct {
  dig.In

  Handlers []Handler `group:"server"`
}

func NewServer(p ServerParams) *Server {
  server := newServer()
  for _, h := range p.Handlers {
    server.Register(h)
  }
  return server
}

注意:这个组值中的值是无序的

可以在内嵌了dig.Out 的结构体中使用多个切片来为组提供值,但是考虑到组是以切片的形式来注入值的,这意味着在后面在内嵌了 dig.In的结构体中是以切片的切片形式注入的。从 dig v1.9.0 开始,如果您想为组提供单个元素而不是切片本身,可以在内嵌了dig.Out 的结构体中的向组添加值的切片后面添加 flatten 修饰符。(这个写的有点绕,意思是你如果是用切片来向容器中添加值的,到时dig给你注入的就是切片的切片(二维数组),可以通过加上flatten 修饰符,dig会将这个向容器中添加的切片展开为单个元素,然后后续使用这个组值时给你注入的就是切片了(一维数组))

type IntResult struct {
  dig.Out

  Handler []int `group:"server"`         // [][]int from dig.In
  Handler []int `group:"server,flatten"` // []int from dig.In
}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!