在Golang中玩转依赖注入-dig篇

什么是依赖注入?有时候一个结构体非常复杂,包含了非常多各种类型的属性,这些属性又包含了更多的属性,当我们创建这样一个结构体时需要编写大量的代码。面向接口编程可以让我们的代码避免耦合更具扩展性,但统一更换接口实现时需要大范围的修改代码。

依赖注入帮助我们解决类似的问题,依赖注入框架能够自动解析依赖关系,帮助我们自动构建结构体实例。依赖注入可以对接口注入实例,让整个代码系统不用关注具体的接口实现。

由于Go语言静态的特性,依赖注入在Go中应用并不广泛,主要有两种实现方式:代码生成和反射。

以前介绍过Go依赖注入的中代码生成实现的典型代表wire,如果还没阅读可以点击👉 golang中的依赖注入之wire

本篇来介绍下反射实现的代表dig

安装

因为不需要命令行操作,因此只单独引入dig库即可

arduino 复制代码
go get 'go.uber.org/dig@v1'

快速入门

dig中也有一些概念需要理解

容器

依赖注入容器就是用来解析各种结构体依赖关系的对象,或者简单点理解,他就是一个dig实例

css 复制代码
c := dig.New()

Provide

wire中的provider概念类似(没看过wire也没关系),它就是一个普通的工厂函数,函数返回值是要被创建的类型,函数入参可以有多个,用来描述创建返回值需要依赖的其他类型。

go 复制代码
type UserGateway struct {
  conn *sql.DB
}
​
func (g UserGateway) GetUserName(id string) (name string, err error) {
  row := g.conn.QueryRow("select name from users")
  err = row.Scan(&name)
  return
}
​
err := c.Provide(func(conn *sql.DB) (*UserGateway, error) {
  return &UserGateway{conn}, nil
})
if err != nil {
  // ...
}

上面代码的意思是,创建UserGateway依赖一个conn *sql.DB

那怎么创建conn *sql.DB呢?当然是提供一个conn *sql.DBprovider

go 复制代码
err = c.Provide(func(opt *Option) (*sql.DB, error) {
  return sql.Open(opt.driver, opt.dsn)
})
if err != nil {
  // ...
}

以此类推,我们还得提供一个opt *Optionprovider

go 复制代码
err = c.Provide(func() *Option {
  return &Option{
    driver: "mysql",
    dsn:    "user:password@/dbname",
  }
})
if err != nil {
  // ...
}

于是构建UserGateway条件就全部满足了

Invoke

现在我们在依赖注入容器描述了如何创建每一个类型,以及创建每一个类型依赖的其他类型了。如果想要使用容器中的实例就变得非常简单了

go 复制代码
err := c.Invoke(func(g *UserGateway) {
  name, err := g.GetUserName("liang")
  if err != nil {
    panic(err)
  }
​
  fmt.Println(name)
})
​
if err != nil {
  panic(err)
}

当需要使用g *UserGateway时,只需要像示例代码一样,放入Invoker入参中,dig会根据依赖关系正确创建g *UserGateway实例并完成函数的执行

应用场景

官方文档中提到了dig的推荐场景:

  • 被集成到应用框架中
  • 在服务启动阶段就完成全部依赖关系解析,不推荐在服务启动后再使用dig

下面示例是一个使用dig依赖注入的web服务器,并且在启动阶段就完成了全部的依赖注入

完整代码可以参考:github.com/liangwt/not...

go 复制代码
package main
​
import (
  "github.com/gin-gonic/gin"
  "github.com/liangwt/note/golang/demo/dig/internal"
)
​
func main() {
  // provider代码简略
  c := internal.Init()
​
  r := gin.Default()
​
  err := c.Invoke(func(g *internal.UserGateway) {
    r.GET("/get_username", func(c *gin.Context) {
      name, err := g.GetUserName(c.Query("id"))
      if err != nil {
        c.JSON(500, err.Error())
        return
      }
​
      c.JSON(200, gin.H{
        "name": name,
      })
    })
  })
  if err != nil {
    panic(err)
  }
​
  r.Run()
}

进阶用法

参数对象和返回值对象

前面提到provider函数的入参和返回值可以是多个,但当参数或返回值太多时,可读性较差

go 复制代码
// internal.go
type PostGateway struct {
  conn *sql.DB
}
​
type CommentGateway struct {
  conn *sql.DB
}
​
type UserGateway struct {
  conn *sql.DB
}
​
err = c.Provide(func(Logger *log.Logger, DB *sql.DB) (
  Comments *CommentGateway,
  Posts *PostGateway,
  Users *UserGateway,
  err error,
) {
  return &CommentGateway{conn: DB},
    &PostGateway{conn: DB},
    &UserGateway{conn: DB},
    nil
})
if err != nil {
  // ...
}
go 复制代码
// main.go
func main() {
  c := internal.Init()
​
  err := c.Invoke(func(g *internal.UserGateway) {
    name, err := g.GetUserName("id")
    if err != nil {
      fmt.Println(err)
      return
    }
​
    fmt.Println(name)
  })
  if err != nil {
    panic(err)
  }
}

我们可以传递结构体来增强可读性。和普通结构体参数/返回值不一样,dig要求的结构体必须内嵌dig.In或者dig.Out,下面的示例和上面的代码是等价的

go 复制代码
type Gateways struct {
  dig.Out
​
  Comments *CommentGateway
  Posts    *PostGateway
  Users    *UserGateway
}
​
type Connection struct {
  dig.In
​
  Logger *log.Logger
  DB     *sql.DB
}
​
err = c.Provide(func(conn Connection) (Gateways, error) {
  return Gateways{
    Comments: &CommentGateway{conn: conn.DB},
    Posts:    &PostGateway{conn: conn.DB},
    Users:    &UserGateway{conn: conn.DB},
  }, nil
})
if err != nil {
  // ...
}

在Invoke时也没有区别,依旧可以直接使用UserGateway而不是Gateways

go 复制代码
// main.go
func main() {
  c := internal.Init()
​
  err := c.Invoke(func(g *internal.UserGateway) {
    name, err := g.GetUserName("id")
    if err != nil {
      fmt.Println(err)
      return
    }
​
    fmt.Println(name)
  })
  if err != nil {
    panic(err)
  }
}

注意参数对象/返回值对象与普通结构体的区别

🌲 参数对象/返回值对象包含dig.Indig.Out,虽然是一个结构体,但代表的是多个参数/返回值,结构体的每一个字段都代表着参数/返回值

对于上面的例子,Gateways因为包含dig.Out,所以Invoke时可以注入*CommentGateway*PostGateway*UserGateway

而下面例子Gateways不包含dig.Out只是普通的结构体,Invoke时只能注入Gateways,注入*UserGateway将会报错

go 复制代码
type Gateways struct {
​
  Comments *CommentGateway
  Posts    *PostGateway
  Users    *UserGateway
}
​
type Connection struct {
  dig.In
  
  Logger *log.Logger
  DB     *sql.DB
}
​
err = c.Provide(func(conn Connection) (Gateways, error) {
  return Gateways{
    Comments: &CommentGateway{conn: conn.DB},
    Posts:    &PostGateway{conn: conn.DB},
    Users:    &UserGateway{conn: conn.DB},
  }, nil
})
if err != nil {
  // ...
}
​
func main() {
  c := internal.Init()
​
  err := c.Invoke(func(g internal.Gateways) {
    name, err := g.Users.GetUserName("id")
    if err != nil {
      fmt.Println(err)
      return
    }
​
    fmt.Println(name)
  })
  if err != nil {
    panic(err)
  }
}

🌲 参数对象/返回值对象不能是指针类型,普通类型是可以是指针类型的

可选依赖

有些依赖并不是必须的,因此可以通过结构体tag:optional:"true"把某些依赖标记成可选的。可选依赖只能在参数对象上使用

如下的示例中,即使没有提供*redis.Client的provide函数,Gateways依旧可以被创建成功

go 复制代码
type UserGateway struct {
  conn  *sql.DB
  cache *redis.Client
}
​
func (g *UserGateway) GetUserName(id string) (name string, err error) {
  if g.cache != nil {
    name := g.cache.Get(id).Val()
    if name != "" {
      return name, nil
    }
  }
​
  row := g.conn.QueryRow("select name from users")
  err = row.Scan(&name)
  if err != nil && g.cache != nil {
    g.cache.Set(id, name, -1)
  }
  return
}
​
type Connection struct {
  dig.In
​
  Logger *log.Logger
  Cache  *redis.Client `optional:"true"`
  DB     *sql.DB
}
​
err = c.Provide(func(conn Connection) (Gateways, error) {
  return Gateways{
    Comments: &CommentGateway{conn: conn.DB},
    Posts:    &PostGateway{conn: conn.DB},
    Users:    &UserGateway{conn: conn.DB, cache: conn.Cache},
  }, nil
})
if err != nil {
  // ...
}

命名值

有时候我们可能同时依赖两个相同类型的资源,例如读写分离的库

go 复制代码
type UserGateway struct {
  roDB  *sql.DB
  rwDB  *sql.DB
  cache *redis.Client
}
​
func (g *UserGateway) GetUserName(id string) (name string, err error) {
  if g.cache != nil {
    name := g.cache.Get(id).Val()
    if name != "" {
      return name, nil
    }
  }
​
  row := g.roDB.QueryRow("select name from users")
  err = row.Scan(&name)
  if err != nil && g.cache != nil {
    g.cache.Set(id, name, -1)
  }
  return
}

以上场景无论我们怎么提供两个*sql.DB的provider,都无法区分出来哪一个是只读库,哪一个是读写库

go 复制代码
err = c.Provide(func(opt *Option) (*sql.DB, *sql.DB, error) {
  // 读写库
  rw, err := sql.Open(opt.driver, opt.dsn)
  if err != nil {
    return nil, nil, err
  }
  
  // 只读库
  ro, err := sql.Open("mysql", "user:password@/ro_dbname")
  if err != nil {
    return nil, nil, err
  }
​
  return rw, ro, nil
})
if err != nil {
  // ...
}
​
////////////////以下和上文代码等价////////////////
​
// 读写库
err = c.Provide(func(opt *Option) (*sql.DB, error) {
  return sql.Open(opt.driver, opt.dsn)
})
if err != nil {
  // ...
}
​
// 只读库
err = c.Provide(func() (*sql.DB, error) {
  return sql.Open("mysql", "user:password@/ro_dbname")
})
if err != nil {
  // ...
}

在构建*UserGateway时,rwDB, roDB也无法对应以上两个*sql.DB

go 复制代码
err = c.Provide(func(Logger *log.Logger, rwDB, roDB *sql.DB) (
  Comments *CommentGateway,
  Posts *PostGateway,
  Users *UserGateway,
  err error,
) {
  return &CommentGateway{rwDB: rwDB},
    &PostGateway{rwDB: rwDB},
    &UserGateway{rwDB: rwDB, roDB: roDB},
    nil
})
if err != nil {
  // ...
}

解决方案就是命名

首先我们要做的是分别给两个*sql.DB一个名字,用来区分两个*sql.DB

如果各自使用独立的函数,则可以使用dig.Name

go 复制代码
// 读写库
err = c.Provide(func(opt *Option) (*sql.DB, error) {
  return sql.Open(opt.driver, opt.dsn)
}, dig.Name("rw"))
if err != nil {
  // ...
}
​
// 只读库
err = c.Provide(func() (*sql.DB, error) {
  return sql.Open("mysql", "user:password@/ro_dbname")
}, dig.Name("ro"))
if err != nil {
  // ...
}

如果使用一个函数构建两个*sql.DB,则可以使用返回值对象的tag

go 复制代码
type DBResult struct {
	dig.Out

	RWDB *sql.DB `name:"rw"`
	RODB *sql.DB `name:"ro"`
}

err = c.Provide(func(opt *Option) (DBResult, error) {
	rw, err := sql.Open(opt.driver, opt.dsn)
	if err != nil {
		return DBResult{}, err
	}

	ro, err := sql.Open("mysql", "user:password@/ro_dbname")
	if err != nil {
		return DBResult{}, err
	}

	return DBResult{RWDB: rw, RODB: ro}, nil
})
if err != nil {
	// ...
}

通过以上两种方式,我们声明了两个*sql.DB,哪一个是只读库,哪一个是读写库。下面要做的就是使用这两个库是指定对应关系。于是RODB *sql.DB便会被注入只读库。

go 复制代码
type Connection struct {
  dig.In
​
  Logger *log.Logger
  Cache  *redis.Client `optional:"true"`
  RODB   *sql.DB       `name:"ro"`
  RWDB   *sql.DB       `name:"rw"`
}
​
type Gateways struct {
  dig.Out
​
  Comments *CommentGateway
  Posts    *PostGateway
  Users    *UserGateway
}
err = c.Provide(func(conn Connection) (Gateways, error) {
  return Gateways{
    Comments: &CommentGateway{rwDB: conn.RWDB},
    Posts:    &PostGateway{rwDB: conn.RWDB},
    Users:    &UserGateway{rwDB: conn.RWDB, roDB: conn.RODB, cache: conn.Cache},
  }, nil
})
if err != nil {
  // ...
}

总结

本篇介绍了利用反射实现的依赖注入框架dig

可以看到dig在使用被注入的依赖时,需要放入Invoke的函数中,如果在代码中任意使用,必然大幅影响代码的可读性。和wire不同,如果缺少某项依赖dig在编译阶段不会报错,只有在运行时的panic

基于以上两点,我更推荐在服务启动阶段就利用dig完成全部依赖关系解析,不推荐在服务启动后再使用dig

dig也被集成到了github.com/uber-go/fx的框架中,有机会我也会给大家介绍下fx框架

以上所有的示例的代码都可以在github.com/liangwt/not...找到


✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的后端知识 ✨

相关推荐
喵叔哟28 分钟前
【.NET 8 实战--孢子记账--从单体到微服务】--简易权限--访问权限中间件
微服务·中间件·.net
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@1 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端
幸运小圣1 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
菜菜-plus2 小时前
分布式,微服务,SpringCloudAlibaba,nacos,gateway,openFeign
java·分布式·微服务·nacos·gateway·springcloud·openfeign
前端SkyRain2 小时前
后端Node学习项目-用户管理-增删改查
后端·学习·node.js
提笔惊蚂蚁3 小时前
结构化(经典)软件开发方法: 需求分析阶段+设计阶段
后端·学习·需求分析
老猿讲编程3 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
黄小耶@3 小时前
python如何使用Rabbitmq
分布式·后端·python·rabbitmq