GoLong的学习之路(番外)如何使用依赖注入工具:wire
我为什么要直接写番外呢?其原因很简单。项目中会使用,其实在这里大家就可以写一些项目来了。
依赖注入的工具本质思想其实都大差不差。无非控制反转和依赖注入。
文章目录
- 控制反转
- 为什么需要依赖注入工具
- wire的概念
- 提供者(provider)
- Injector(注入器)
- `注意`
- wire的使用
- 特性
- 绑定接口
- 结构体提供者
- 指针结构体传入的中
- `注入MyFoo字段`
- 重要
- 绑定值
- 接口值
- 使用结构的字段作为提供者
- Cleanup函数
控制反转
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)
(还有一种方式通过依赖查找。这个我在我之前的Spring的文章中有写,感兴趣的朋友可以移步)。
依赖注入是生成灵活和松散耦合代码的标准技术,通过明确地向组件提供它们所需要的所有依赖关系。
在 Go 中通常采用将依赖项作为参数传递给构造函数的形式:
构造函数NewBookRepo在创建BookRepo时需要从外部将依赖项db作为参数传入,我们在NewBookRepo中无需关注db的创建逻辑,实现了代码解耦。
// NewBookRepo 创建BookRepo的构造函数
func NewBookRepo(db *gorm.DB) *BookRepo {return &BookRepo{db: db}
}
对于控制反转来说,如果在NewBookPepo 函数中自行创建相关依赖,使得代码的耦合度比较高,并且难以维护和调试。
为了解决这个问题,大佬们就开始想办法,在还华中尽可能的使用控制反转和依赖注入将程序解耦合开,从而写出灵活,并易于测试的程序。
为什么需要依赖注入工具
在小型应用程序中,我们可以自行创建依赖并手动注入。但是在一个大型应用程序中,手动去实现所有依赖的创建和注入就会比较繁琐。
为了方便管理业务,和技术分层,会在实际中划分住不同的代码层。其中MVC就是一个非常常见的业务思想。
例如:
HTTP服务中:
这中模型是最为常见的的模型。

服务需要有一个配置,指定工作模式、连接的数据库和监听端口等信息。(conf)
目录: conf/conf.go
// conf/conf.go// NewDefaultConfig 返回默认配置,不需要依赖
func NewDefaultConfig() *Config {...}
这里定义了一个默认配置,当然后续可以支持从配置文件或环境变量读取配置信息
在程序的data层,需要定义一个连接数据库的函数,它依赖上面定义的Config并返回一个*gorm.DB(这里使用gorm连接数据库)
目录:data/data.go
// data/data.go// NewDB 返回数据库连接对象
func NewDB(cfg *conf.Config) (*gorm.DB, error) {...}
同时定义一个BookRepo,它有一些数据操作相关的方法。它的构造函数NewBookRepo依赖*gorm.DB,并返回一个*BookRepo。
目录:data/data.go
// data/data.gotype BookRepo struct {db *gorm.DB
}func NewBookRepo(db *gorm.DB) *BookRepo {...}
Service层位于data层和Server层的中间,它负责实现对外服务。其中构造函数 NewBookService 依赖Config和BookRepo
目录:service/service.go
// service/service.gotype BookService struct {config *conf.Configrepo *data.BookRepo
}func NewBookService(cfg *conf.Config, repo *data.BookRepo) *BookService {...}
server层又有一个NewServer构造函数,它依赖外部传入Config和BookService
目录:server/server.go
// server/server.gotype Server struct {config *conf.Configservice *service.BookService
}func NewServer(cfg *conf.Config, srv *service.BookService) *Server {...}
在main.go文件中又依赖Server创建一个app
目录:main.go
// main.gotype Server interface {Run()
}type App struct {server Server
}func newApp(server Server) *App {...}
由于在程序中定义了大量需要依赖注入的构造函数,程序的main函数中会出现以下情形。
目录:main.go
// main.gofunc main() {cfg := conf.NewDefaultConfig()db, _ := data.NewDB(cfg)repo := data.NewBookRepo(db)bookSrv := service.NewBookService(cfg, repo)server := server.NewServer(cfg, bookSrv)app := newApp(server)app.Run()
}
所有依赖的创建和顺序都需要手动维护。
故我们就需要一个工具来解决这个问题。
wire的概念
Go社区中有很多依赖注入框架。比如:Uber的dig和Facebook的inject都使用反射来做运行时依赖注入。
Wire 是一个的 Google 开源的依赖注入工具,通过自动生成代码的方式在编译期完成依赖注入。
wire中有两个核心概念:提供者(provider)和注入器(injector)
提供者(provider)
提供者函数可以分组为提供者函数集(provider set)。使用wire.NewSet 函数可以将多个提供者函数添加到一个集合中。如果经常同时使用多个提供者函数,这非常有用。
package demoimport (// ..."github.com/google/wire"
)// ...var ProviderSet = wire.NewSet(NewX, NewY, NewZ)
而这个集合也可以作为提供者函数。
package demoimport (// ..."example.com/some/other/pkg"
)
var MegaSet = wire.NewSet(ProviderSet, pkg.OtherSet)
而提供者函数可以实现这几种方式。
- 可以产生值的普通函数
type X struct {Value int
}// NewX 返回一个X对象
func NewX() X {return X{Value: 7}
}
- 可以使用参数指定依赖项
type Y struct {Value int
}// NewY 返回一个Y对象,需要传入一个X对象作为依赖。
func NewY(x X) Y {return Y{Value: x.Value+1}
}
- 可以返回错误的
type Z struct {Value int
}// NewZ 返回一个Z对象,当传入依赖的value为0时会返回错误。
func NewZ(ctx context.Context, y Y) (Z, error) {if y.Value == 0 {return Z{}, errors.New("cannot provide z when value is zero")}return Z{Value: y.Value + 2}, nil
}
Injector(注入器)
应用程序中是用一个注入器来连接提供者,注入器就是一个按照依赖顺序调用提供者。
使用 wire时,你只需要编写注入器的函数签名,然后 wire会生成对应的函数体
要声明一个注入器函数只需要在函数体中调用wire.Build
这个函数的返回值也无关紧要,只要它们的类型正确即可。这些值在生成的代码中将被忽略。
假设上面的提供者函数是在一个名为 wire_demo/demo 的包中定义的,下面将声明一个注入器来得到一个Z函数
package mainimport ("context""github.com/google/wire""wire_demo/demo"
)
func initZ(ctx context.Context) (demo.Z, error) {wire.Build(demo.ProviderSet)return demo.Z{}, nil
}
wire.Build的参数和wire.NewSet一样:都是提供者集合。这些就在该注入器的代码生成期间使用的提供者集。
将上面的代码保存到wire.go中,文件最上面的//go:build wireinject 是必须的(Go 1.18之前的版本使用// +build wireinject),它确保wire.go不会参与最终的项目编译。
注意
在实际运用中我要根据实际业务层,封装不同的wrie的go类,这样方便管理。在哪里调用什么清晰明了
wire的使用
安装wire命令行工具。
命令行:> go install github.com/google/wire/cmd/wire@latest
在wire.go同级目录下执行以下命令: wire
wire会在同级目录下wire_gen.go文件中生成注入器的具体实现。
生成代码:-----》
// Code generated by Wire. DO NOT EDIT.//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinjectpackage mainimport ("context""wire_demo/demo"
)// Injectors from wire.go:func initZ(ctx context.Context) (demo.Z, error) {x := demo.NewX()y := demo.NewY(x)z, err := demo.NewZ(ctx, y)if err != nil {return demo.Z{}, err}return z, nil
}
从生成的内容可以看出,wire生成的内容非常接近开发人员自己编写的内容。
此外,运行时对wire的依赖性很小:所有编写的代码都只是普通的Go代码,可以在没有wire的情况下使用。
特性
绑定接口
依赖项注入通常用于绑定接口的具体实现。
wire通过类型标识将输入与输出匹配,因此倾向于创建一个返回接口类型的提供者。这不是习惯写法,因为Go的最佳实践是返回具体类型。
你可以在提供者集中声明接口绑定:
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,
)
wire.Bind的第一个参数是指向所需接口类型值的指针,第二个参数是指向实现该接口的类型值的指针。任何包含接口绑定的集合还必须具有提供具体类型的提供者。
结构体提供者
可以使用提供的类型构造结构体。
使用wire.Struct函数构造一个结构体类型,并告诉注入器应该注入哪个字段。
注入器将使用字段类型的提供程序填充每个字段。
type Foo int
type Bar intfunc ProvideFoo() Foo {/* ... */}func ProvideBar() Bar {/* ... */}type FooBar struct {MyFoo FooMyBar Bar
}var Set = wire.NewSet(ProvideFoo,ProvideBar,wire.Struct(new(FooBar), "MyFoo", "MyBar"),
)
这个wire会生成一个类似于:
func injectFooBar() FooBar {foo := ProvideFoo()bar := ProvideBar()fooBar := FooBar{MyFoo: foo,MyBar: bar,}return fooBar
}
wire.Struct的第一个参数是指向所需结构体类型的指针,随后的参数是要注入的字段的名称。可以使用一个特殊的字符串“*”作为快捷方式,告诉注入器注入结构体的所有字段。
指针结构体传入的中
对于生成的结构体类型S,wire.struct同时提供S和*S
注入MyFoo字段
var Set = wire.NewSet(ProvideFoo,wire.Struct(new(FooBar), "MyFoo"),
)
1.生成的类似于:
func injectFooBar() FooBar {foo := ProvideFoo()fooBar := FooBar{MyFoo: foo,}return fooBar
}
2.生成的类似于:
func injectFooBar() *FooBar {foo := ProvideFoo()fooBar := &FooBar{MyFoo: foo,}return fooBar
}
重要
有时防止结构体的某些字段被注入器填充很有必要,尤其是在将*传递给wire.Struct的时候。你可以用wire:"-"标记字段,使wire忽略这些字段。
type Foo struct {mu sync.Mutex `wire:"-"`Bar Bar
}
使用wire.Struct(new(Foo), "*")提供Foo类型时,wire将自动省略mu字段。
此外,在wire.Struct(new(Foo), "mu")中显式指定被忽略的字段也会报错。
绑定值
有时,将基本值(通常为nil)绑定到类型是有用的。
你可以向提供程序集添加一个值表达式,而不是让注入器依赖于一次性提供者函数。
type Foo struct {X int
}func injectFoo() Foo {wire.Build(wire.Value(Foo{X: 42}))return Foo{}
}
生成的注入器:
func injectFoo() Foo {foo := _wireFooValuereturn foo
}var (_wireFooValue = Foo{X: 42}
)
值得注意的是,表达式将被复制到注入器的包中。
对变量的引用将在注入器包的初始化过程中进行计算。如果表达式调用任何函数或从任何通道接收任何函数,wire 将会报错。
接口值
对于接口值,使用 InterfaceValue。
func injectReader() io.Reader {wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))return nil
}
使用结构的字段作为提供者
用户想要的提供程序是结构的某些字段,如果发现自己在下面的示例中编写了一个类似getS的提供者,可以尝试将结构字段作为所提供的类型:
type Foo struct {S stringN intF float64
}func getS(foo Foo) string {// Bad! Use wire.FieldsOf instead.return foo.S
}func provideFoo() Foo {return Foo{ S: "Hello, World!", N: 1, F: 3.14 }
}func injectedMessage() string {wire.Build(provideFoo,getS,)return ""
}
可以使用wire.FieldsOf直接使用结构体的字段,而无需编写一个类似getS的函数:
func injectedMessage() string {wire.Build(provideFoo,wire.FieldsOf(new(Foo), "S"),)return ""
}
生成为:
func injectedMessage() string {foo := provideFoo()string2 := foo.Sreturn string2
}
可以根据需要将任意多的字段名称添加到wire.FieldsOf中
Cleanup函数
如果提供程序创建了一个需要清理的值(例如关闭文件、关闭数据库连接等),那么它可以返回一个闭包来清理资源。
注入器将使用它向调用方返回聚合清理函数,或者在注入器实现中稍后调用的提供程序返回错误时清理资源。
func provideFile(log Logger, path Path) (*os.File, func(), error) {f, err := os.Open(string(path))if err != nil {return nil, nil, err}cleanup := func() {if err := f.Close(); err != nil {log.Log(err)}}return f, cleanup, nil
}
注意
cleanup函数的签名必须是func(),并且保证在提供者的任何输入的cleanup函数之前调用。
总而言之这个番外,还是蛮简单的。
相关文章:
GoLong的学习之路(番外)如何使用依赖注入工具:wire
我为什么要直接写番外呢?其原因很简单。项目中会使用,其实在这里大家就可以写一些项目来了。 依赖注入的工具本质思想其实都大差不差。无非控制反转和依赖注入。 文章目录 控制反转为什么需要依赖注入工具 wire的概念提供者(provider&#x…...
【pyspider】爬取ajax请求数据(post),如何处理python2字典的unicode编码字段?
情景:传统的爬虫只需要设置fetch_typejs即可,因为可以获取到整个页面。但是现在ajax应用越来越广泛,所以有的网页不能用此种爬虫类型来获取页面的数据,只能用slef.crawl()来发起http请求来抓取数据。 直接上例子: 可以…...
torch.cumprod实现累乘计算
cumprod取自“cumulative product”的缩写,即“累计乘法”。 数学公式为: y i x 1 x 2 x 3 . . . x i y_ix_1\times{x_2}\times{x_3}\times{...}\times{x_i} yix1x2x3...xi 官方链接:torch.cumprod 用法: impo…...
设计模式之迭代器模式
什么是迭代器模式 迭代器模式(Iterator pattern)是一种对象行为型设计模式,它提供了一种方法来顺序访问聚合对象中的元素,而又不暴露该对象的内部表示,同时也可以将迭代逻辑与聚合对象的实现分离,增强了代码…...
使用SSH ,让windows和linux互通
简介 SSH 是一种安全网络协议,旨在让客户端和服务器之间进行安全的数据传输。SSH 的核心思想是利用公钥加密技术和共享密钥加密技术相结合的方式,使客户端和服务器之间建立起安全的连接。 当客户端发起连接请求时,服务器会对客户端进行身份验…...
常用设计模式——策略模式
策略模式是什么 策略模式(Strategy):针对一组算法,将每一个算法封装起来,从而使得它们可以相互替换。 比如我们一个软件的会员等级,每一个等级都会有对应的一些等级权益,那么每一个等级权益就…...
牛客网 CM11.链表分割
目录 1.解题思路2.代码实现 1.解题思路 此题目思路相对简单,利用双指针,一个指针指向小于val的,一个指针指向大于等于val的,但实现起来,如果仅仅使用单链表,那么还需特别判断第一个指针是否为空从而特意做…...
[iOS开发]iOS中TabBar中间按钮凸起的实现
在日常使用app的过程中,经常能看到人家实现了底部分栏控制器的中间按钮凸起的效果,那么这是怎么实现的呢? 效果演示: 实现原理: 创建按钮 创建一个UITabBar的子类,重写它的layoutSubviews方法࿱…...
数字时代,企业的数据共享意味着什么?
随着数字化整体在社会方方面面的推进,通过数据直接或间接创造的价值越来越大,逐渐成为了构建现代社会的重要要素。而对于企业来说,数据也是在数字经济中容易接触也切实能够利用产生大量价值,所以如何最大化利用数据,让…...
壹[1],QT自定义控件创建(QtDesigner)
1,环境 Qt 5.14.2 VS2022 原因:厌烦了控件提升的繁琐设置,且看不到界面预览显示。 2,QT制作自定义控件 2.1,New/其他项目/Qt4 设计师自定义控件 2.2,设置项目名称 2.3,设置 2.4,设…...
解决Java对接LDAP AD域登录出现Unprocessed Continuation Reference(s)错误
出现该错误的原因,主要是因为Java namingx的库,默认选项是未设置跟随,389返回的是AD域条目的引用,需要进行引用跟随。 解决方法分为两种,第一类不使用全局目录服务的端口389和636,而是使用真实端口 把代码…...
could not read ok from ADB Server
执行adb devices提示 List of devices attached * daemon not running; starting now at tcp:5037 could not read ok from ADB Server * failed to start daemon 方法1,关闭防火墙, could not read ok from ADB Server_夜星辰2023的博客-CSDN博客 我…...
超越基础:Flutter 中 onTap 的 5 条规则让你脱颖而出
小事情决定了你的熟练程度,这些小细节的有趣之处在于它们的丰富性。您将在代码库中的数百个位置遇到 onTap 事件。增强它们可以对代码的可维护性和最终用户体验产生重大的积极影响。 onTap 就是这样一个微小但丰富的东西——我们在每个屏幕上都使用它。这纯粹是关于…...
综合布线可视化管理系统价值分析
传统综合布线管理,全部依靠手工登记,利用标签标示线缆,利用文档资料记录链路的连接和变更,高度依赖网络管理员的管理能力,维护效率低下。同时,网络接入故障和非法接入难以及时发现。在以往的文章中小编一直…...
【JavaSE】基础笔记 - 类和对象(上)
目录 1、面向对象的初步认知 1.1、什么是面向对象 1.2、面向对象与面向过程 2. 类定义和使用 2.1、简单认识类 2.2、类的定义格式 2.3、自定义类举例说明 2.3.1、定义一个狗类 2.3.2、定义一个学生类 3、类的实例化 3.1、什么是实例化 3.2、类和对象的说明 1、面向…...
浅谈开口互感器在越南美的工业云系统中的应用
摘 要:分析低压开口式电流互感器的原理,结合工程实例分析开口电流互感器在低压配电系统中,主要是改造项目中的应用及施工细节,为用户快速实现智能配电提供解决方案,该方案具有成本低、投资少、安装接线简便等优点&…...
docker的使用以及注意事项
ssh的登录 1.登录ssh ssh 用户名IP地址 2.生成密钥 ssh-keygen生成密钥,在.ssh文件夹中(如果没有自己生成一个) 如果密钥之前已经生成过,可能在配置git的时候,会报错:这个密钥已经使用过的报错 解决方法是:otherwise[…...
大数据之LibrA数据库系统告警处理(ALM-12027 主机PID使用率超过阈值)
告警解释 系统每30秒周期性检测PID使用率,并把实际PID使用率和阈值进行比较,PID使用率默认提供一个阈值。当检测到PID使用率超出阈值时产生该告警。 平滑次数为1,主机PID使用率小于或等于阈值时,告警恢复;平滑次数大…...
软考 系统架构设计师系列知识点之数字孪生体(3)
接前一篇文章:软考 系统架构设计师系列知识点之数字孪生体(2) 所属章节: 第11章. 未来信息综合技术 第5节. 数字孪生体技术概述 3. 数字孪生体的关键技术 建模、仿真和基于数据融合的数字线程是数字孪生体的三项核心技术。能够做…...
新闻稿的写作注意事项!纯干货
新闻稿是企业、机构、政府等组织向公众传递信息的重要途径之一,也是媒体获取新闻素材的主要来源。一篇优质的新聞稿不仅可以吸引读者的注意力,还可以提高组织的形象和声誉。因此写好新闻稿至关重要。下面伯乐网络传媒来给大家探讨一些新闻稿写作的注意事…...
Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
