Golang 依赖注入设计哲学|12.6K 的依赖注入库 wire

一、前言

线上项目往往依赖非常多的具备特定能力的资源,如:DB、MQ、各种中间件,以及随着项目业务的复杂化,单一项目内,业务模块也逐渐增多,如何高效、整洁管理各种资源十分重要。

本文从“术”层面,讲述“依赖注入”的实现,带你体会其对于整洁架构 & DDD 等设计思想的落地,起到的支撑作用。

涉及内容:

  • 最热门的 golang 依赖注入库,GitHub 🌟 12.5k:https://github.com/google/wire

  • GiuHub 🌟 22.5k 的 golang 微服务框架 kratos 默认使用 wire 作为依赖注入方式:https://github.com/go-kratos/kratos

  • Spring Boot 与 Golang 的依赖注入对比

  • 依赖注入的设计哲学

📺 B站账号:白泽talk,绝大部分博客内容都将会通过视频讲解,不过文章一般是先于视频发布

image-20240703002016429

白泽的开源 Golang 学习仓库:https://github.com/BaiZe1998/go-learning,用于文章归档 & 聚合博客代码案例

公众号【白泽talk】,本期内容的 pdf 版本,可以关注公众号,回复【依赖注入】获得,往期资源的获取,都是类似的方式。

二、What

📒 本文所涉及编写的代码,已收录于 https://github.com/BaiZe1998/go-learning/di 目录

一句话概括:实例 A 的创建,依赖于实例 B 的创建,且在实例 A 的生命周期内,持有对实例 B 的访问权限。

2.1 案例分析

依赖注入(Dependency Injection, DI),以 Golang 为例,左侧为手动完成依赖注入,右侧为不使用依赖注入

🌟 不使用依赖注入风险:

  1. 全局变量十分不安全,存在覆写的可能
  2. 资源散落在各处,可能重复创建,浪费内存,后续维护能力极差
  3. 提高循环依赖的风险
  4. 全局变量的引入提高单元测试的成本

image-20240625222009500

  • 不使用依赖注入 demo
package mainvar (mysqlUrl = "mysql://blabla"// 全局数据库实例db = NewMySQLClient(mysqlUrl)
)func NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return "data"
}func NewApp() *App {return &App{}
}type App struct {
}func (a *App) GetData(query string, args ...interface{}) string {data := db.Exec(query, args...)return data
}// 不使用依赖注入
func main() {app := NewApp()rest := app.GetData("select * from table where id = ?", "1")println(rest)
}
  • 手动依赖注入 demo
package mainfunc NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return "data"
}func NewApp(client *MySQLClient) *App {return &App{client: client}
}type App struct {// App 持有唯一的 MySQLClient 实例client *MySQLClient
}func (a *App) GetData(query string, args ...interface{}) string {data := a.client.Exec(query, args...)return data
}// 手动依赖注入
func main() {client := NewMySQLClient("mysql://blabla")app := NewApp(client)rest := app.GetData("select * from table where id = ?", "1")println(rest)
}

三、Why

依赖注入 (Dependency Injection,缩写为 DI),可以理解为一种代码的构造模式(就是写法),按照这样的方式来写,能够让你的代码更加容易维护。

四、How

4.1 Golang 依赖注入

以 Golang 🌟 最多的开源库 wire 为例讲解:https://github.com/google/wire/blob/main/docs/guide.md

wire是由 google 开源的一个供 Go 语言使用的依赖注入代码生成工具。它能够根据你的代码,生成相应的依赖注入 go 代码。

而与其它依靠反射实现的依赖注入工具不同的是,wire 能在编译期(准确地说是代码生成时)如果依赖注入有问题,在代码生成时即可报出来,不会拖到运行时才报,更便于 debug。

  • Install:
go install github.com/google/wire/cmd/wire@latest
  • provider: a function that can produce a value

以上面手动实现依赖注入为基础,wire 做的工作是帮助开发者完成如下组装过程

client := NewMySQLClient("mysql://blabla")
app := NewApp(client)

而其中用到的 NewMySQLClient、NewApp 在 wire 定义为一个个的 provider,是需要提前由开发者实现的。

func NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}func NewApp(client *MySQLClient) *App {return &App{client: client}
}

假设系统中的资源很多,配置很多,出现了如下复杂的初始化流程,人工完成依赖注入则变得复杂:

a := NewA(xxx, yyy) error
b := NewB(ctx, a) error
c := NewC(zzz, a, b) error
d := NewD(www, kkk, a) error
e := NewD(ctx, b, d) error
  • injector: a function that calls providers in dependency order

如下是名为 wire.go 的依赖注入配置文件,是一个只会被 wire 命令行工具处理的 injector 文件,用于声明依赖注入流程。

wire.go:

//go:build wireinject
// +build wireinject// The build tag makes sure the stub is not built in the final build.package mainimport "github.com/google/wire"// wireApp init application.
func wireApp(url string) *App {wire.Build(NewMySQLClient, NewApp)return nil
}

执行 wire 命令,则在当前目录下生成 wire_gen.go 文件,此时的 wireApp 函数,就等价于最初手动编写的依赖注入流程,可以在真正需要初始化的引入。

wire_gen.go:

// Code generated by Wire. DO NOT EDIT.//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinjectpackage main// Injectors from wire.go:// wireApp init application.
func wireApp(url string) *App {mySQLClient := NewMySQLClient(url)app := NewApp(mySQLClient)return app
}

4.2 针对复杂项目的依赖注入设计哲学

这里以 go-kratos 的模版项目为例讲解,是一个 helloworld 服务,我们着重分析其借助 wire 进行依赖注入的部分。

以下 helloworld 模板服务的 interanl 目录的内容:

.
├── biz
│   ├── README.md
│   ├── biz.go
│   └── greeter.go
├── conf
│   ├── conf.pb.go
│   └── conf.proto
├── data
│   ├── README.md
│   ├── data.go
│   └── greeter.go
├── server
│   ├── grpc.go
│   ├── http.go
│   └── server.go
└── service├── README.md├── greeter.go└── service.go

各个目录的关系如图:

image-20240702235735708

  • data:业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口,data 偏重业务的含义,它所要做的是将领域对象重新拿出来。

  • biz:业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,repo 接口在这里定义,使用依赖倒置的原则。

  • service:实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑。

  • server:为http和grpc实例的创建和配置,以及注册对应的 service 。

🌟上图右侧部分,表示了模块之间的依赖关系,可以看到,依赖的注入是逆向的,资源往往被业务模块持有,业务模块则被负责编排业务的应用持有,应用则被负责对外通信的模块持有。

此时在服务启动前的实例化阶段,provider 的定义和注入,本质是这样一种状态:

func main() {dbClient := NewDBClient()dataN := NewDataN(dbClient)dataM := NewDataM(dbClient)bizA := NewBizA(dataN)bizB := NewBizB(dataM)bizC := NewBizC(dataN, dataM)serviceX := NewService(bizA, bizB, bizC)server := NewServer(serviceX)server.httpXXX // 提供 http 服务server.grpcXXX // 提供 grpc 服务
}

在 helloworld 这个 demo 当中,则是这样定义 provider 的:

// biz 目录
var ProviderSet = wire.NewSet(NewGreeterUsecase)type GreeterUsecase struct {repo GreeterRepolog  *log.Helper
}func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)return uc.repo.Save(ctx, g)
}// data 目录
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)type Data struct {// TODO wrapped database client
}func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {cleanup := func() {log.NewHelper(logger).Info("closing the data resources")}return &Data{}, cleanup, nil
}type greeterRepo struct {data *Datalog  *log.Helper
}func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {return &greeterRepo{data: data,log:  log.NewHelper(logger),}
}
// service 目录
var ProviderSet = wire.NewSet(NewGreeterService)type GreeterService struct {v1.UnimplementedGreeterServeruc *biz.GreeterUsecase
}func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {return &GreeterService{uc: uc}
}func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})if err != nil {return nil, err}return &v1.HelloReply{Message: "Hello " + g.Hello}, nil
}// server 目录
var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)func NewGRPCServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *grpc.Server {var opts = []grpc.ServerOption{grpc.Middleware(recovery.Recovery(),),}if c.Grpc.Network != "" {opts = append(opts, grpc.Network(c.Grpc.Network))}if c.Grpc.Addr != "" {opts = append(opts, grpc.Address(c.Grpc.Addr))}if c.Grpc.Timeout != nil {opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))}srv := grpc.NewServer(opts...)v1.RegisterGreeterServer(srv, greeter)return srv
}

在 helloworld 这个 demo 当中,则是这样定义 injector 的:

// wire.go
func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

最后运行 wire 的到的完成注入的文件如下:

// wire_gen.go
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {dataData, cleanup, err := data.NewData(confData, logger)if err != nil {return nil, nil, err}greeterRepo := data.NewGreeterRepo(dataData, logger)greeterUsecase := biz.NewGreeterUsecase(greeterRepo, logger)greeterService := service.NewGreeterService(greeterUsecase)grpcServer := server.NewGRPCServer(confServer, greeterService, logger)httpServer := server.NewHTTPServer(confServer, greeterService, logger)app := newApp(logger, grpcServer, httpServer)return app, func() {cleanup()}, nil
}

生成代码之后,则可以像使用普通的 golang 函数一样,使用这个 wire_gen.go 文件内的 wireApp 函数实例化一个 helloworld 服务

func main() {flag.Parse()logger := log.With(log.NewStdLogger(os.Stdout),// ...)c := config.New(// ...)defer c.Close()// ...app, cleanup, err := wireApp(bc.Server, bc.Data, logger)if err != nil {panic(err)}defer cleanup()// start and wait for stop signalif err := app.Run(); err != nil {panic(err)}
}

4.3 wire 的更多用法

参见 wire 的文档,自己用几遍就明白了,这里举几个例子:

  • 定义携带 error 返回值的 provider
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {if bar.X == 0 {return Baz{}, errors.New("cannot provide baz when bar is zero")}return Baz{X: bar.X}, nil
}
  • provider 集合:方便组织多个 provider
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
  • 接口绑定:
type Fooer interface {Foo() string
}type MyFooer stringfunc (b *MyFooer) Foo() string {return string(*b)
}func provideMyFooer() *MyFooer {b := new(MyFooer)*b = "Hello, World!"return b
}type Bar stringfunc provideBar(f Fooer) string {// f will be a *MyFooer.return f.Foo()
}var Set = wire.NewSet(provideMyFooer,wire.Bind(new(Fooer), new(*MyFooer)),provideBar)

五、对比 Spring Boot 的依赖注入

Spring Boot的依赖注入(DI)和Golang开源库Wire的依赖注入在设计思路上存在一些相同点和不同点。以下是对这些相同点和不同点的分析:

相同点
  1. 降低耦合度:两者都通过依赖注入的方式实现了代码的松耦合。这意味着,一个对象不需要显式地创建或查找它所依赖的其他对象,这些依赖项会由外部容器(如Spring容器)或工具(如Wire)自动提供。
  2. 提高可测试性:由于依赖关系被解耦,可以更容易地替换依赖项以进行单元测试。无论是Spring Boot还是使用Wire的Golang应用,都可以轻松地为组件提供模拟或存根的依赖项以进行测试。
  3. 灵活性:两者都允许在不修改组件代码的情况下替换依赖项。这使得应用程序在维护和扩展时更加灵活。
不同点
  1. 实现方式
    • Spring Boot的依赖注入是基于Java的反射机制和Spring框架的容器管理功能实现的。Spring容器负责创建和管理Bean的生命周期,并在需要时自动注入依赖项,核心在于运行时
    • Wire是一个Golang的代码生成工具,它通过分析代码中的构造函数和结构体标签,自动生成依赖注入的代码(减少人工工作量),在开发阶段已经通过工具生成好了依赖注入的代码,程序编译时,资源之间的依赖关系已经固定。
  2. 配置方式
    • Spring Boot的依赖注入通常通过配置文件(如application.properties或application.yml)和注解(如@Autowired)进行配置。开发者可以在配置文件中定义Bean的属性,并通过注解在需要注入的地方指明依赖关系。
    • Wire则通过特殊的Go文件(通常是wire.go文件)来定义类型之间的依赖关系。这些文件包含了用于生成依赖注入代码的指令和元数据。
  3. 运行时开销
    • Spring Boot的依赖注入在运行时需要依赖Spring容器来管理Bean的生命周期和依赖关系。这可能会引入一些额外的运行时开销,特别是在大型应用程序中。
    • Wire在编译时生成依赖注入的代码,因此它在运行时没有额外的开销。这使得使用Wire的Golang应用程序通常具有更好的性能。

六、参考资料

kratos:https://go-kratos.dev/en/docs/getting-started/start/

wire:https://github.com/google/wire/blob/main/_tutorial/README.md

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/39685.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安全和加密常识(1)对称加密和非对称加密以及相应算法

文章目录 对称加密(Symmetric Encryption)非对称加密(Asymmetric Encryption)使用场景和优缺点对称加密和非对称加密是信息安全领域中两种重要的加密方式,它们分别使用不同的加密算法和密钥管理方式来保护数据的机密性。下面我来简单介绍一下它们及其相应的算法。 对称加…

js多线程解决方案Web Worker简单说明与实例介绍

Web Worker是JavaScript中的多线程解决方案,它允许在后台执行耗时的任务,而不会阻塞主线程。通过将任务发送给Web Worker,主线程可以继续执行其他操作,同时Web Worker在后台完成任务。 使用Web Worker的一般流程如下:…

爆火AI惨遭阉割,1600万美国年轻人集体「失恋」? Character AI被爆资金断裂,00后炸了

【新智元导读】最近,在美国00后中爆火的Character AI,竟然把聊天机器人对话模型给「阉割」了?愤怒的年轻人们冲进社区,抱怨的声浪快要掀翻天了!而这背后,似乎还有谷歌或Meta的授意。 美国当今最火爆的社交…

JavaScript——数组的遍历和多维数组

目录 任务描述 相关知识 数组的遍历 多维数组的实现 编程要求 任务描述 本关任务:将一个一维数组转换为二维数组,行优先。 相关知识 数组的遍历 数组的遍历指按顺序访问你数组的每一个元素。有两种方法: 使用for循环 //依次在浏览器…

看个病都有大模型陪诊了!上海市第一人民医院主导,一手体验在此

现在看个病,都有大模型全程陪诊了。 这是上海市第一人民医院此时此时正在发生的事情—— AI就医助理,无需下载APP,打开支付宝就能用。 从诊前预约挂号、在线取号,到诊中院内导航、排队叫号、扫码支付,再到诊后的报告…

基于SpringBoot高校体育运动会管理系统设计和实现(源码+LW+调试文档+讲解等)

💗博主介绍:✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者,博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 🌟文末获取源码数据库🌟 感兴趣的可以先收藏起来,…

Vuetify3:监听当前手机还是电脑

我们在开发的时候&#xff0c;实现根据移动设备或PC设备来改编一些交互样式&#xff0c;这个时候我们需要监听宽度&#xff0c;在Vuetify3中可我们可以参考 ​​​​显示 & 平台配合监听即可在窗口缩小的时候判断出手机还是电脑 <template><v-app><div v-if…

KVB交易平台 :市场迎来新热潮!铜价会持续上涨吗?

近期&#xff0c;全球铜价出现明显上涨趋势。韩国光阳LME仓库的铜库存显著下降&#xff0c;市场对即时需求的增加作出了积极反应。供应端的紧张和需求端的复苏共同推动了铜价的上涨。 KVB外汇 分析师们对未来铜价保持谨慎乐观态度&#xff0c;认为长期内铜价有望保持稳定甚至进…

unity 打开文件资源管理器(不使用UnityEditor)

问题描述&#xff1a; 有个地方需要用户上传一张图片&#xff0c;网上查了&#xff0c;使用了如下代码&#xff1a; string imagePath EditorUtility.OpenFilePanel("Select Image", "", "png,jpg,jpeg");然后在打包发布时遇到问题&#xff0c…

浏览器关闭页面 前的提示

原生html <script>window.addEventListener("beforeunload", function (e) {// 取消事件的默认动作e.preventDefault();// Chrome 需要设置 returnValuee.returnValue "2333333333";});</script>React useEffect(() > {const handleBefor…

【面试系列】UI设计师高频面试题及详细解答

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题. ⭐️ AIGC时代的创新与未来&#xff1a;详细讲解AIGC的概念、核心技术、…

守望先锋2延迟高、卡顿、丢包的解决方法一览

守望先锋2/ow2是一款在全球范围内拥有超高热度的特殊游戏&#xff0c;因为该作在一定程度上是FPS游戏融合了MOBA元素&#xff0c;守望先锋2中的每一位英雄之间都有克制因素&#xff0c;使其技能点形成闭环逻辑&#xff0c;提升了游戏的可重复游戏性和趣味性。不过很多玩家在游玩…

三菱A系列网络连接

寄存器名 读写 寄存器类型 变量类型 寄存器范围 说明 X##1 R/W BIT I/O离散 0&#xff0d;7FF Input Y##1 R/W BIT I/O离散 0&#xff0d;7FF Output M##1 R/W BIT I/O离散 0&#xff0d;9255 Internal relay B##1 R/W BIT I/O离散 0&#xff0d;3FF Link relay F##1 R/W BIT I…

电子邮件OTP验证身份认证接口API服务商比较

电子邮件OTP验证身份认证接口API服务商如何正确选择&#xff1f; 电子邮件OTP验证是一种广泛应用且安全的身份认证方式。AokSend将比较几家主要的电子邮件OTP验证身份认证接口API服务商&#xff0c;帮助企业选择合适的解决方案。 电子邮件OTP&#xff1a;验证优势 可以为用户…

【硬件模块】SGP30气体传感器

SGP30 这是SGP30官方文档里开头的介绍&#xff0c;简单来说就是SGP30是一个数字多像素气体传感器&#xff0c;然后具有长期稳定性和低漂移。 这些我们都不用管&#xff0c;我们只需要知道SGP30是通过I2C来通信的&#xff0c;并且可以采集的数据有CO2和TVOC的含量。TVOC是“To…

RPM包管理-rpm命令管理

1.RPM包命令原则 所有的rpm包都在光盘中 例&#xff1a;httpd-2.2.15-15.e16.centos.1.i686.rpm httpd 软件包名 2.2.15 软件版本 15 软件发布的次数 e16.centos 适合的Linux平台 i686 适合的硬件平台…

【Git 学习笔记】1.3 Git 的三个阶段

1.3 Git 的三个阶段 由于远程代码库后续存在新的提交&#xff0c;因此实操过程中的结果与书中并不完全一致。根据书中 HEAD 指向的 SHA-1&#xff1a;34acc370b4d6ae53f051255680feaefaf7f7850d&#xff0c;可通过以下命令切换到对应版本&#xff0c;并新建一个 newdemo 分支来…

Linux——shell原理和文件权限

1.shell原理 在我们使用云服务器时&#xff0c;需要通过shell进行使用&#xff0c;而shell则是一种外壳程序。 我们提到过&#xff0c;大部分的指令实际上就是文件&#xff0c;当用户需要执行某种功能时&#xff0c;由于用户不擅长和操作系统直接交互&#xff08;操作复杂&…

度量监控平台,研发的助手

背景 指标度量遥测数据准确、多维度和可观测统一管控台入口(SLS/ARMS日志查询&#xff0c;OpenTelemetry/SkyWalking&#xff0c;Grafana)Trace、Metric、Log (链路、指标、日志) 目标 快速排障&#xff0c;解决问题的助手整体联动&#xff0c;降低使用成本梳理排查路径&…

ble广播数据(Payload)类型详解

BLE&#xff08;Bluetooth Low Energy&#xff09;广播数据类型在BLE通信中扮演着重要角色&#xff0c;它们用于在广播包中携带各种设备信息和服务信息。 一、广播数据类型概述 BLE广播数据通常包含在一个或多个广播数据单元&#xff08;AD Structure&#xff09;中&#xff…