Golang 依赖注入:dig

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: Golang 依赖注入:dig

一、简介


go 是否需要依赖注入库曾经是一个饱受争议的话题。实际上是否需要依赖注入,取决于编程风格。依赖注入是一种编程模式。比较适合面向对象编程,在函数式编程中则不需要。go 是一门支持多范式编程的语言,所以在使用面向对象的大型项目中,还是建议按照实际情况判断是否应该使用依赖注入模式。


二、主流的依赖注入库


依赖注入实现的是一件很小的事情,所以单纯实现依赖注入的软件包称不上框架,而只能被称为库。 目前主流的 golang 库非常多,比如 uber 开源的 dig、elliotchance 开源的 dingo、sarulabs 开源的 di、google 开源的 wire 和 facebook 开源的 inject 等等。 目前最受欢迎的是 dig 和 wire,这篇文章主要介绍 dig 的用法。


三、基本用法


创建容器


container := dig.New()

容器用来管理依赖。


注入依赖


调用容器的 Provide 方法,传入一个工厂函数,容器会自动调用该工厂函数创建依赖,并保存到 container 中。


type DBClient struct {}
func NewDBClient() {
    return *DBClient{}
}
func InitDB() *DBClient {
    return NewDBClient()
}
container.Provide(InitDB)

注入的依赖会被 dig 所管理,每种类型的对象只会被创建一次,可以理解为单例。如果再注入同一类型的依赖,工厂函数则不会被执行。


type DBClient struct {}
func NewDBClient() {
    return *DBClient{}
}
func InitDB() *DBClient {
    return NewDBClient()
}
func InitDB2() *DBClient {
    return NewDBClient()
}
container.Provide(InitDB)
container.Provide(InitDB2)// 不会执行


使用依赖


如果需要使用依赖,使用 container 的 Invoke 方法。并在函数的形式参数中定义参数,container 会自动把单例注入。


func UseOption(db *DBClient){
}
container.Invoke(UseOption)


四、参数对象


当某个函数所需要的依赖过多时,可以将参数以对象的方式获取。 假设启动一个服务,需要 ServerConfig、MySQL、Redis、Mongodb 等参数。


container.Provide(func InitHttpServer(svcfg *ServerConfig, mysql *MySQL, redis *Redis, mongodb *Mongodb) *Server{
    // 业务逻辑
    return Server.Run()
})

此时代码可读性会变差,通过 dig.In 将 InitHttpServer 所依赖的 4 个依赖打包成一个对象。


type ServerParams {
    dig.In
    svcfg   *ServerConfig
    mysql   *MySQL
    redis   *Redis
    mongodb *Mongodb
}
container.Provide(func InitHttpServer(p ServerParams) *Server{
    // 业务逻辑
    return Server.Run()
})


五、结果对象


和参数对象类似,如果一个工厂函数返回了多个依赖时,会有相同的问题。不过这种情况比较少见。 假设有个函数返回了启动 InitHttpServer 所需要的所有依赖。


container.Provide(func BuildServerdependences() (*ServerConfig, *MySQL, *Redis, *Mongodb){
    // 业务逻辑
    return svcfg, mysql, redis, mongodb
})

解决这个现象的方式和 dig.In 类似,还有一个 dig.Out,用法一致。


type BuildServerdependences struct {
  dig.Out
    ServerConfig  *ServerConfig
    MySQL         *MySQL
    Redis         *Redis
    Mongodb       *Mongodb
}
container.Provide(func BuildServerdependences() (*ServerConfig, *MySQL, *Redis, *Mongodb){
    // 业务逻辑
    return BuildServerdependences{
        ServerConfig: svcfg,
        MySQL:        mysql,
        Redis:        redis, 
        Mongodb:      mongodb,
    }
})


六、可选依赖


如果在 Provide 的工厂函数或者 Invoke 的函数中所需要的依赖不存在,dig 会抛出异常。 假设 container 中没有 Mongo.Config 类型的依赖,那么就会抛出异常。


func InitDB(cfg *Mongo.Config) *Mongo.Client{
    return Mongo.NewClient(cfg)
}
container.Invoke(InitDB)// 抛出异常
通过在标签后标注 optional 为 true 的方式可以允许某个依赖不存在,这时传入的是 nil。
go
复制代码
type InitDBParams struct {
  dig.In
    mongoConfig *Mongo.Config `optional:"true"`
}
func InitDB(p InitDBParams) *Mongo.Client {
    // p.mongoConfig 是 nil
    return Mongo.NewClient(cfg)
}
container.Invoke(InitDB)// 继续执行


七、命名


注入命名依赖


由于默认是单例的,如果需要两个相同类型的实例怎么办?比如现在需要两台 Mongodb 客户端。 dig 提供了对象命名功能,在调用 Provide 时传入第二个参数就可以进行区分。


type MongodbClient struct {}
func NewMongoClient(cfg *Mongo.Client) *MongodbClient{
    return ConnectionMongo(cfg)
}
container.Provide(NewMongoClient, dig.Name("mgo1"))
container.Provide(NewMongoClient, dig.Name("mgo2"))

除了传递 dig.Name 参数以外,如果使用了结果对象的话,可以通过设置 name 标签来实现,效果一致。


type MongodbClients struct {
  dig.In
    Mgo1 *MongodbClient `name:"mgo1"`
    Mgo2 *MongodbClient `name:"mgo2"`
}


使用命名依赖


不论是 Provide 还是Invoke,都只能通过参数对象的方式使用命名依赖。使用方式是通过 tag,和注入时一致。


type MongodbClients struct {
  dig.Out
    Mgo1 *MongodbClient `name:"mgo1"`
    Mgo2 *MongodbClient `name:"mgo2"`
}
container.Invoke(func (mcs MongodbClients) {
    mcs.Mgo1
    mcs.Mgo2
})


注意事项


嵌套 dig.In 的结构体的所有字段必须都在 container 中存在,否则 Invoke 中传入的函数不会被调用。 错误示例:


type MongodbClients struct {
  dig.Out
    Mgo1 *MongodbClient `name:"mgo1"`
    Mgo2 *MongodbClient `name:"mgo2"`
}
container.Provide(NewMongoClient, dig.Name("mgo1"))// 只注入了 mgo1
container.Invoke(func (mcs MongodbClients) {// 需要依赖 mgo1 和 mgo2,所以不会执行
    mcs.Mgo1
    mcs.Mgo2
})


八、组


除了给依赖命名,还可以给依赖分组,相同类型的依赖会放入同一个切片中。 不过分组后就不能通过命名的方式访问依赖了,也就是说命名和组同时只能采用一种方式。 还是上面那个例子。


使用参数


type MongodbClient struct {}
func NewMongoClient(cfg *Mongo.Client) *MongodbClient{
    return ConnectionMongo(cfg)
}
container.Provide(NewMongoClient, dig.Group("mgo"))
container.Provide(NewMongoClient, dig.Group("mgo"))


使用结果对象


type MongodbClientGroup struct {
  dig.In
    Mgos []*MongodbClient `group:"mgo"`
}


使用组


组只能通过对象参数的方式使用。


container.Invoke(func(mcg MongodbClientGroup) {
    for _, m := range mcg {
        // 业务逻辑
    }
})


注意事项


和命名依赖相似,嵌套 dig.In 的结构体所有带有 gruop 标签的字段必须都在 container 中至少存在一个,否则 Invoke 中传入的函数不会被调用。 除此之外,group 返回的切片不保证注入时的依赖顺序,也就是说依赖切片是无序的。


九、使用组和命名的方式启动多个 http 服务


到现在已经学完了 dig 所有的 API,下面稍微实战一下,通过 dig 的依赖组启动多个服务。


package main
import (
  "errors"
  "fmt"
  "net/http"
  "strconv"
  "go.uber.org/dig"
)
type ServerConfig struct {
  Host string // 主机地址
  Port string // 端口号
  Used bool   // 是否被占用
}
type ServerGroup struct {
  dig.In
  Servers []*Server `group:"server"`
}
type ServerConfigNamed struct {
  dig.In
  Config1 *ServerConfig `name:"config1"`
  Config2 *ServerConfig `name:"config2"`
  Config3 *ServerConfig `name:"config3"`
}
type Server struct {
  Config *ServerConfig
}
func (s *Server) Run(i int) {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(write http.ResponseWriter, req *http.Request) {
    write.Write([]byte(fmt.Sprintf("第%s个服务,端口号是: %s", strconv.FormatInt(int64(i), 10), s.Config.Port)))
  })
  http.ListenAndServe(s.Config.Host+":"+s.Config.Port, mux)
}
func NewServerConfig(port string) func() *ServerConfig {
  return func() *ServerConfig {
    return &ServerConfig{Host: "127.0.0.1", Port: port, Used: false}
  }
}
func NewServer(sc ServerConfigNamed) *Server {
  if !sc.Config1.Used {
    sc.Config1.Used = true
    return &Server{Config: sc.Config1}
  } else if !sc.Config2.Used {
    sc.Config2.Used = true
    return &Server{Config: sc.Config2}
  } else if !sc.Config3.Used {
    sc.Config3.Used = true
    return &Server{Config: sc.Config3}
  }
  panic(errors.New(""))
}
func ServerRun(sg ServerGroup) {
  for i, s := range sg.Servers {
    go s.Run(i)
  }
}
func main() {
  container := dig.New()
  // 注入 3 个服务配置项
  container.Provide(NewServerConfig("8199"), dig.Name("config1"))
  container.Provide(NewServerConfig("8299"), dig.Name("config2"))
  container.Provide(NewServerConfig("8399"), dig.Name("config3"))
  // 注入 3 个服务实例
  container.Provide(NewServer, dig.Group("server"))
  container.Provide(NewServer, dig.Group("server"))
  container.Provide(NewServer, dig.Group("server"))
  // 使用缓冲 channel 卡住主协程
  serverChan := make(chan int, 1)
  container.Invoke(ServerRun)
  <-serverChan
}

运行该文件,可以通过访问 http://127.0.0.1:8199http://127.0.0.1:8299http://127.0.0.1:8399 查看效果。 上面的示例使用命名依赖并不合适,但是为了演示 API 的实际使用,所以使用了命名依赖。 没有哪种 API 是最好的。在实际开发中,根据具体业务,使用最适合场景的 API,灵活运用即可。


十、参考链接


pak dig pkg.go.dev

Dependency Injection in Go software is fun

依赖注入不是Java的专利,Golang也有 知乎 老钱

uber dig github

Go 每日一库之 dig 大俊



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
存储 JSON NoSQL
Golang 依赖注入:入门篇
Golang 依赖注入:入门篇
147 0
|
Go 前端开发 开发者
Golang 依赖注入(Dependency Injection)
在各种大工程中少不了各种测试,其中 TDD 就是非常流行的一种,在前端开发中用的比较多的 [Jest](https://github.com/facebook/jest) 就是一种,在 Golang 开发命令行工具的时候也是需要 DI 这种模式来实现命令行测试的。
1846 0
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
101 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
63 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
98 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
85 4
|
2月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
45 3
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
47 4
Golang语言goroutine协程篇
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
49 3
Golang语言之Prometheus的日志模块使用案例
|
2月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
55 3
Golang语言之函数(func)进阶篇
下一篇
无影云桌面