Golang 依赖注入:dig

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 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 依赖注入:入门篇
133 0
|
Go 前端开发 开发者
Golang 依赖注入(Dependency Injection)
在各种大工程中少不了各种测试,其中 TDD 就是非常流行的一种,在前端开发中用的比较多的 [Jest](https://github.com/facebook/jest) 就是一种,在 Golang 开发命令行工具的时候也是需要 DI 这种模式来实现命令行测试的。
1808 0
|
3月前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
217 0
|
4天前
|
监控 Serverless Go
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
|
26天前
|
测试技术 Shell Go
Golang质量生态建设问题之Go语言的单元测试的问题如何解决
Golang质量生态建设问题之Go语言的单元测试的问题如何解决
|
3月前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
159 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
3月前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
191 5
|
3月前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
99 6
|
3月前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
59 3
|
3月前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
381 5