接触 Golang 有一段时间了,发现 Golang 同样需要类似 Java 中 Spring 一样的依赖注入框架。如果项目规模比较小,是否有依赖注入框架问题不大,但当项目变大之后,有一个合适的依赖注入框架是十分必要的。通过调研,了解到 Golang 中常用的依赖注入工具主要有 Inject 、Dig 等。但是今天主要介绍的是 Go 团队开发的 Wire,一个编译期实现依赖注入的工具。
说起依赖注入
就要引出另一个名词控制反转
( IoC )。IoC 是一种设计思想,其核心作用是降低代码的耦合度。依赖注入
是一种实现控制反转
且用于解决依赖性问题的设计模式。
举个例子,假设我们代码分层关系是 dal 层连接数据库,负责数据库的读写操作。那么我们的 dal 层的上一层 service 负责调用 dal 层处理数据,在我们目前的代码中,它可能是这样的:
// dal/user.go
func (u *UserDal) Create(ctx context.Context, data *UserCreateParams) error {
db := mysql.GetDB().Model(&entity.User{})
user := entity.User{
Username: data.Username,
Password: data.Password,
}
return db.Create(&user).Error
}
// service/user.go
func (u *UserService) Register(ctx context.Context, data *schema.RegisterReq) (*schema.RegisterRes, error) {
params := dal.UserCreateParams{
Username: data.Username,
Password: data.Password,
}
err := dal.GetUserDal().Create(ctx, params)
if err != nil {
return nil, err
}
registerRes := schema.RegisterRes{
Msg: "register success",
}
return ®isterRes, nil
}
在这段代码里,层级依赖关系为 service -> dal -> db,上游层级通过 Getxxx
实例化依赖。但在实际生产中,我们的依赖链比较少是垂直依赖关系,更多的是横向依赖。即我们一个方法中,可能要多次调用Getxxx
的方法,这样使得我们代码极不简洁。
不仅如此,我们的依赖都是写死的,即依赖者的代码中写死了被依赖者的生成关系。当被依赖者的生成方式改变,我们也需要改变依赖者的函数,这极大的增加了修改代码量以及出错风险。
接下来我们用依赖注入
的方式对代码进行改造:
// dal/user.go
type UserDal struct{
DB *gorm.DB
}
func NewUserDal(db *gorm.DB) *UserDal{
return &UserDal{
DB: db
}
}
func (u *UserDal) Create(ctx context.Context, data *UserCreateParams) error {
db := u.DB.Model(&entity.User{})
user := entity.User{
Username: data.Username,
Password: data.Password,
}
return db.Create(&user).Error
}
// service/user.go
type UserService struct{
UserDal *dal.UserDal
}
func NewUserService(userDal dal.UserDal) *UserService{
return &UserService{
UserDal: userDal
}
}
func (u *UserService) Register(ctx context.Context, data *schema.RegisterReq) (*schema.RegisterRes, error) {
params := dal.UserCreateParams{
Username: data.Username,
Password: data.Password,
}
err := u.UserDal.Create(ctx, params)
if err != nil {
return nil, err
}
registerRes := schema.RegisterRes{
Msg: "register success",
}
return ®isterRes, nil
}
// main.go
db := mysql.GetDB()
userDal := dal.NewUserDal(db)
userService := dal.NewUserService(userDal)
如上编码情况中,我们通过将 db 实例对象注入到 dal 中,再将 dal 实例对象注入到 service 中,实现了层级间的依赖注入。解耦了部分依赖关系。
在系统简单、代码量少的情况下上面的实现方式确实没什么问题。但是项目庞大到一定程度,结构之间的关系变得非常复杂时,手动创建每个依赖,然后层层组装起来的方式就会变得异常繁琐,并且容易出错。这个时候勇士 wire 出现了!
Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入。它不需要反射机制,后面会看到, Wire 生成的代码与手写无异。
wire 的安装:
go get github.com/google/wire/cmd/wire
上面的命令会在 $GOPATH/bin
中生成一个可执行程序 wire
,这就是代码生成器。可以把$GOPATH/bin
加入系统环境变量 $PATH
中,所以可直接在命令行中执行 wire
命令。
下面我们在一个例子中看看如何使用 wire
。
现在我们有这样的三个类型:
type Message string
type Channel struct {
Message Message
}
type BroadCast struct {
Channel Channel
}
三者的 init 方法:
func NewMessage() Message {
return Message("Hello Wire!")
}
func NewChannel(m Message) Channel {
return Channel{Message: m}
}
func NewBroadCast(c Channel) BroadCast {
return BroadCast{Channel: c}
}
假设 Channel 有一个 GetMsg 方法,BroadCast 有一个 Start 方法:
func (c Channel) GetMsg() Message {
return c.Message
}
func (b BroadCast) Start() {
msg := b.Channel.GetMsg()
fmt.Println(msg)
}
如果手动写代码的话,我们的写法应该是:
func main() {
message := NewMessage()
channel := NewChannel(message)
broadCast := NewBroadCast(channel)
broadCast.Start()
}
如果使用 wire
,我们需要做的就变成如下的工作了:
1 . 提取一个 init 方法 InitializeBroadCast:
func main() {
b := demo.InitializeBroadCast()
b.Start()
}
2 . 编写一个 wire.go 文件,用于 wire 工具来解析依赖,生成代码:
//+build wireinject
package demo
func InitializeBroadCast() BroadCast {
wire.Build(NewBroadCast, NewChannel, NewMessage)
return BroadCast{}
}
注意:需要在文件头部增加构建约束://+build wireinject
3 . 使用 wire 工具,生成代码,在 wire.go 所在目录下执行命令:wire gen wire.go
。会生成如下代码,即在编译代码时真正使用的Init函数:
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
func InitializeBroadCast() BroadCast {
message := NewMessage()
channel := NewChannel(message)
broadCast := NewBroadCast(channel)
return broadCast
}
我们告诉 wire
,我们所用到的各种组件的 init
方法(NewBroadCast
, NewChannel
, NewMessage
),那么 wire
工具会根据这些方法的函数签名(参数类型/返回值类型/函数名)自动推导依赖关系。
wire.go
和 wire_gen.go
文件头部位置都有一个 +build
,不过一个后面是 wireinject
,另一个是 !wireinject
。+build
其实是 Go 语言的一个特性。类似 C/C++ 的条件编译,在执行 go build
时可传入一些选项,根据这个选项决定某些文件是否编译。wire
工具只会处理有wireinject
的文件,所以我们的 wire.go
文件要加上这个。生成的 wire_gen.go
是给我们来使用的,wire
不需要处理,故有 !wireinject
。
Wire
有两个基础概念,Provider
(构造器)和 Injector
(注入器)
Provider
实际上就是生成组件的普通方法,这些方法接收所需依赖作为参数,创建组件并将其返回。我们上面例子的 NewBroadCast
就是 Provider
。Injector
可以理解为 Providers
的连接器,它用来按依赖顺序调用 Providers
并最终返回构建目标。我们上面例子的 InitializeBroadCast
就是 Injector
。下面简单介绍一下 wire
在飞书问卷表单服务中的应用。
飞书问卷表单服务的 project
模块中将 handler 层、service 层和 dal 层的初始化通过参数注入的方式实现依赖反转。通过 BuildInjector
注入器来初始化所有的外部依赖。
dal 伪代码如下:
func NewProjectDal(db *gorm.DB) *ProjectDal{
return &ProjectDal{
DB:db
}
}
type ProjectDal struct {
DB *gorm.DB
}
func (dal *ProjectDal) Create(ctx context.Context, item *entity.Project) error {
result := dal.DB.Create(item)
return errors.WithStack(result.Error)
}
// QuestionDal、QuestionModelDal...
service 伪代码如下:
func NewProjectService(projectDal *dal.ProjectDal, questionDal *dal.QuestionDal, questionModelDal *dal.QuestionModelDal) *ProjectService {
return &projectService{
ProjectDal: projectDal,
QuestionDal: questionDal,
QuestionModelDal: questionModelDal,
}
}
type ProjectService struct {
ProjectDal *dal.ProjectDal
QuestionDal *dal.QuestionDal
QuestionModelDal *dal.QuestionModelDal
}
func (s *ProjectService) Create(ctx context.Context, projectBo *bo.ProjectCreateBo) (int64, error) {}
handler 伪代码如下:
func NewProjectHandler(srv *service.ProjectService) *ProjectHandler{
return &ProjectHandler{
ProjectService: srv
}
}
type ProjectHandler struct {
ProjectService *service.ProjectService
}
func (s *ProjectHandler) CreateProject(ctx context.Context, req *project.CreateProjectRequest) (resp *
project.CreateProjectResponse, err error) {}
injector.go 伪代码如下:
func NewInjector()(handler *handler.ProjectHandler) *Injector{
return &Injector{
ProjectHandler: handler
}
}
type Injector struct {
ProjectHandler *handler.ProjectHandler
// components,others...
}
在 wire.go 中如下定义:
// +build wireinject
package app
func BuildInjector() (*Injector, error) {
wire.Build(
NewInjector,
// handler
handler.NewProjectHandler,
// services
service.NewProjectService,
// 更多service...
//dal
dal.NewProjectDal,
dal.NewQuestionDal,
dal.NewQuestionModelDal,
// 更多dal...
// db
common.InitGormDB,
// other components...
)
return new(Injector), nil
}
执行 wire gen ./internal/app/wire.go
生成 wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
func BuildInjector() (*Injector, error) {
db, err := common.InitGormDB()
if err != nil {
return nil, err
}
projectDal := dal.NewProjectDal(db)
questionDal := dal.NewQuestionDal(db)
questionModelDal := dal.NewQuestionModelDal(db)
projectService := service.NewProjectService(projectDal, questionDal, questionModelDal)
projectHandler := handler.NewProjectHandler(projectService)
injector := NewInjector(projectHandler)
return injector, nil
}
在 main.go 中加入初始化 injector 的方法 app.BuildInjector
injector, err := BuildInjector()
if err != nil {
return nil, err
}
//project服务启动
svr := projectservice.NewServer(injector.ProjectHandler, logOpt)
svr.Run()
注意,如果你运行时,出现了 BuildInjector
重定义,那么检查一下你的 //+build wireinject
与 package app
这两行之间是否有空行,这个空行必须要有!见https://github.com/google/wire/issues/117
NewSet
一般应用在初始化对象比较多的情况下,减少 Injector
里面的信息。当我们项目庞大到一定程度时,可以想象会出现非常多的 Providers。NewSet
帮我们把这些 Providers 按照业务关系进行分组,组成 ProviderSet
(构造器集合),后续只需要使用这个集合即可。
// project.go
var ProjectSet = wire.NewSet(NewProjectHandler, NewProjectService, NewProjectDal)
// wire.go
func BuildInjector() (*Injector, error) {
wire.Build(InitGormDB, ProjectSet, NewInjector)
return new(Injector), nil
}
上述例子的 Provider
都是函数,除函数外,结构体也可以充当 Provider
的角色。Wire
给我们提供了结构构造器(Struct Provider)。结构构造器创建某个类型的结构,然后用参数或调用其它构造器填充它的字段。
// project_service.go
// 函数provider
func NewProjectService(projectDal *dal.ProjectDal, questionDal *dal.QuestionDal, questionModelDal *dal.QuestionModelDal) *ProjectService {
return &projectService{
ProjectDal: projectDal,
QuestionDal: questionDal,
QuestionModelDal: questionModelDal,
}
}
// 等价于
wire.Struct(new(ProjectService), "*") // "*"代表全部字段注入
// 也等价于
wire.Struct(new(ProjectService), "ProjectDal", "QuestionDal", "QuestionModelDal")
// 如果个别属性不想被注入,那么可以修改 struct 定义:
type App struct {
Foo *Foo
Bar *Bar
NoInject int `wire:"-"`
}
Bind
函数的作用是为了让接口类型的依赖参与 Wire
的构建。Wire
的构建依靠参数类型,接口类型是不支持的。Bind
函数通过将接口类型和实现类型绑定,来达到依赖注入的目的。
// project_dal.go
type IProjectDal interface {
Create(ctx context.Context, item *entity.Project) (err error)
// ...
}
type ProjectDal struct {
DB *gorm.DB
}
var bind = wire.Bind(new(IProjectDal), new(*ProjectDal))
构造器可以提供一个清理函数(cleanup),如果后续的构造器返回失败,前面构造器返回的清理函数都会调用。初始化 Injector
之后可以获取到这个清理函数,清理函数典型的应用场景是文件资源和网络连接资源。清理函数通常作为第二返回值,参数类型为 func()
。当 Provider
中的任何一个拥有清理函数,Injector
的函数返回值中也必须包含该函数。并且 Wire
对 Provider
的返回值个数及顺序有以下限制:
4 . 第一个返回值是需要生成的对象
5 . 如果有 2 个返回值,第二个返回值必须是 func() 或 error
6 . 如果有 3 个返回值,第二个返回值必须是 func(),而第三个返回值必须是 error
// db.go
func InitGormDB()(*gorm.DB, func(), error) {
// 初始化db链接
// ...
cleanFunc := func(){
db.Close()
}
return db, cleanFunc, nil
}
// wire.go
func BuildInjector() (*Injector, func(), error) {
wire.Build(
common.InitGormDB,
// ...
NewInjector
)
return new(Injector), nil, nil
}
// 生成的wire_gen.go
func BuildInjector() (*Injector, func(), error) {
db, cleanup, err := common.InitGormDB()
// ...
return injector, func(){
// 所有provider的清理函数都会在这里
cleanup()
}, nil
}
// main.go
injector, cleanFunc, err := app.BuildInjector()
defer cleanFunc()
更多用法具体可以参考 wire官方指南:https://github.com/google/wire/blob/main/docs/guide.md
接着我们就用上述的这些 wire
高级特性对 project
服务进行代码改造:
project_dal.go
type IProjectDal interface {
Create(ctx context.Context, item *entity.Project) (err error)
// ...
}
type ProjectDal struct {
DB *gorm.DB
}
// wire.Struct方法是wire提供的构造器,"*"代表为所有字段注入值,在这里可以用"DB"代替
// wire.Bind方法把接口和实现绑定起来
var ProjectSet = wire.NewSet(
wire.Struct(new(ProjectDal), "*"),
wire.Bind(new(IProjectDal), new(*ProjectDal)))
func (dal *ProjectDal) Create(ctx context.Context, item *entity.Project) error {}
dal.go
// DalSet dal注入
var DalSet = wire.NewSet(
ProjectSet,
// QuestionDalSet、QuestionModelDalSet...
)
project_service.go
type IProjectService interface {
Create(ctx context.Context, projectBo *bo.CreateProjectBo) (int64, error)
// ...
}
type ProjectService struct {
ProjectDal dal.IProjectDal
QuestionDal dal.IQuestionDal
QuestionModelDal dal.IQuestionModelDal
}
func (s *ProjectService) Create(ctx context.Context, projectBo *bo.ProjectCreateBo) (int64, error) {}
var ProjectSet = wire.NewSet(
wire.Struct(new(ProjectService), "*"),
wire.Bind(new(IProjectService), new(*ProjectService)))
service.go
// ServiceSet service注入
var ServiceSet = wire.NewSet(
ProjectSet,
// other service set...
)
handler 伪代码如下:
var ProjectHandlerSet = wire.NewSet(wire.Struct(new(ProjectHandler), "*"))
type ProjectHandler struct {
ProjectService service.IProjectService
}
func (s *ProjectHandler) CreateProject(ctx context.Context, req *project.CreateProjectRequest) (resp *
project.CreateProjectResponse, err error) {}
injector.go 伪代码如下:
var InjectorSet = wire.NewSet(wire.Struct(new(Injector), "*"))
type Injector struct {
ProjectHandler *handler.ProjectHandler
// others...
}
wire.go
// +build wireinject
package app
func BuildInjector() (*Injector, func(), error) {
wire.Build(
// db
common.InitGormDB,
// dal
dal.DalSet,
// services
service.ServiceSet,
// handler
handler.ProjectHandlerSet,
// injector
InjectorSet,
// other components...
)
return new(Injector), nil, nil
}
wire 不允许不同的注入对象拥有相同的类型。google 官方认为这种情况,是设计上的缺陷。这种情况下,可以通过类型别名来将对象的类型进行区分。
例如服务会同时操作两个 Redis 实例,RedisA & RedisB
func NewRedisA() *goredis.Client {...}
func NewRedisB() *goredis.Client {...}
对于这种情况,wire 无法推导依赖的关系。可以这样进行实现:
type RedisCliA *goredis.Client
type RedisCliB *goredis.Client
func NewRedisA() RedicCliA {...}
func NewRedisB() RedicCliB {...}
5.2 单例问题
依赖注入的本质是用单例来绑定接口和实现接口对象间的映射关系。而通常实践中不可避免的有些对象是有状态的,同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。针对这种场景我们通常设计多层的 DI 容器来实现单例隔离,亦或是脱离 DI 容器自行管理对象的生命周期。
Wire 是一个强大的依赖注入工具。与 Inject 、Dig 等不同的是,Wire只生成代码而不是使用反射在运行时注入,不用担心会有性能损耗。项目工程化过程中,Wire 可以很好协助我们完成复杂对象的构建组装。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/yHB9BzEGIki1fyjYojdpYQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。