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 依赖ConfigBookRepo
目录:service/service.go

// service/service.gotype BookService struct {config *conf.Configrepo   *data.BookRepo
}func NewBookService(cfg *conf.Config, repo *data.BookRepo) *BookService {...}

server层又有一个NewServer构造函数,它依赖外部传入ConfigBookService
目录: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)

而提供者函数可以实现这几种方式。

  1. 可以产生值的普通函数
type X struct {Value int
}// NewX 返回一个X对象
func NewX() X {return X{Value: 7}
}
  1. 可以使用参数指定依赖项
type Y struct {Value int
}// NewY 返回一个Y对象,需要传入一个X对象作为依赖。
func NewY(x X) Y {return Y{Value: x.Value+1}
}
  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的第一个参数是指向所需结构体类型的指针,随后的参数是要注入的字段的名称。可以使用一个特殊的字符串“*”作为快捷方式,告诉注入器注入结构体的所有字段。

指针结构体传入的中

对于生成的结构体类型Swire.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函数之前调用。

总而言之这个番外,还是蛮简单的。

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

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

相关文章

【计算机网络笔记】TCP的拥塞控制机制

系列文章目录 什么是计算机网络? 什么是网络协议? 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能(1)——速率、带宽、延迟 计算机网络性能(2)…

Linux文件描述符和打开文件之间的关系

简介 文件描述符和打开的文件之间似乎呈现出一一对应的关系。然而,实际并非如此。多个文件描述符指向同一打开文件,这既有可能,也属必要。这些文件描述符可在相同或不同的进程中打开。 要理解具体情况如何,需要查看由内核维护的…

应用安全四十二:SSO安全

一、什么是SSO SSO是单点登录(Single Sign On)的缩写,是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是比较流行的企业业务整合的解决方案之一。 身份验证过程依赖于双方之间的信任关…

Spring Boot创建多模块项目

创建一个普通的Spring Boot项目, 然后只留下 pom.xml 剩下的都删掉 删除多余标签 标识当前为父模块 创建子模块 删除子模块中多余标签 声明父模块 在父模块中声明子模块

基于JAVA+SSM的房屋租赁系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景介绍: 随着社会的发展和人们…

【C/C++】什么是POD(Plain Old Data)类型

2023年11月6日,周一下午 目录 POD类型的定义标量类型POD类型的特点POD类型的例子整数类型:C 风格的结构体:数组:C 风格的字符串:std::array:使用 memcpy 对 POD 类型进行复制把POD类型存储到文件中,并从文…

webgoat-(A1)SQL Injection

SQL Injection (intro) SQL 命令主要分为三类: 数据操作语言 (DML)DML 语句可用于请求记录 (SELECT)、添加记录 (INSERT)、删除记录 (DELETE) 和修改现有记录 &#xff…

【自我提升】项目升级-Beyond Compare效率工具

写在前面:最近接手一个项目,这个项目集成了许多开源项目,其中我需要对其中一个开源项目进行升级操作。在此记录升级过程和心得,希望可以给各位道友提供一种思路,同时也希望道友有更好的办法能分享在评论区。 项目场景 …

RabiitMQ消息队列系统

一、MQ 1、概念: MQ 全称为 Message Queue (消息队列),是一种应用程序对应用程序的通信方法。MQ 允许应用程序将消息写入队列,其他应用程序从队列中读取并处理这些消息,不需要它们之间直接相互联系。消息队列可用于实现异步通信…

idea Error: java: OutOfMemoryError: insufficient memory处理

IDEA设置里,修改heap size更大一点,可以解决问题

apachesolr启动带调试

这里solr.cmd报错,报错原因是java版本问题,后面发现这是因为多个java版本导致读取java_home失败, 那么我们修改solr.cmd中的JAVA_HOME为SOLR_JAVA_HOME IF DEFINED SOLR_JAVA_HOME set "JAVA_HOME%SOLR_JAVA_HOME%"环境变量将SOLR…

【Python基础】史上最全||一篇博客搞懂Python面向对象编程(封装、继承、多态)

Python面向对象编程 1.面向对象概念介绍1) 面相过程 —— 怎么做?2)面向对象 谁来做 2.类和对象2.1类2.2对象2.3类和对象的关系2.4类的设计2.5面向对象设计案例 士兵类设计2.6身份运算符 3.私有属性和私有方法3.1. 应用场景及定义方式 4.继承、多态重写父…

2014年亚太杯APMCM数学建模大赛C题公共基础课教师专业化培养方式研究求解全过程文档及程序

2014年亚太杯APMCM数学建模大赛 C题 公共基础课教师专业化培养方式研究 原题再现 近年来,世界基础工业、信息产业、服务业的跨越式发展引发了大量人才需求,导致了职业教育的飞速发展,除原有专科层次高等职业教育院校外,大量普通…

24PL-18-50-1836、12PN-4.1-50-1836比例电磁铁控制器

12PG-2.2-40-1836、24PG-8.8-40-1836、12PT-40-1836、24PT-40-1836、12PL-2.2-40-1836、24PL-8.8-40-1836、12PN-2.2-40-1836、24PN-8.8-40-1836、12PG-4.1-50-1836、24PG-18-50-1836、12PT-4.1-50-1836、24PT-18-50-1836、12PL-4.1-50-1836、24PL-18-50-1836、12PN-4.1-50-183…

基于8086汽车智能小车控制系统

**单片机设计介绍,基于8086汽车智能小车控制系统 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于 8086 的汽车智能小车控制系统是一种将微处理器技术应用于汽车控制的系统。下面是其主要的设计介绍: 硬…

大数据学习之Spark性能优化

文章目录 Spark三种任务提交模式宽依赖和窄依赖StageSpark Job的三种提交模式 Shuffle机制分析未优化的Hash Based Shuffle优化后的Hash Based ShuffleSort-Based Shuffle Spark之checkpointcheckpoint概述checkpoint与持久化的区别checkPoint的使用checkpoint源码分析 Spark程…

Django实战项目-学习任务系统-发送短信通知

接着上期代码内容,继续完善优化系统功能。 本次增加发送短信通知功能,学习任务系统发布的任务,为了更加及时通知到学生用户,再原有发送邮件通知基础上,再加上手机短信通知功能。 第一步:开通短信通知服务…

Docker容器中执行throttle.sh显示权限报错:RTNETLINK answers: Operation not permitted

在模拟通信环境时,我执行了一下命令: bash ./throttle.sh wan但是,出现了权限的报错:RTNETLINK answers: Operation not permitted 解决方案说简单也挺简单,只需要两步完成。但是其实又蛮繁琐,因为需要将…

正点原子嵌入式linux驱动开发——Linux 4G通信

前面学习了如何在Linux中使用有线网络或者WIFI,但是使用有线网络或者WIFI有很多限制,因为要布线,即使是WIFI也得先布线,然后再接个无线路由器。有很多场合是不方便布线的,这个时候就是4G大显身手的时候,产品…

答题小程序排位pk答题怎么玩

答题小程序排位PK答题升级赛是一个备受喜爱的功能,它不仅增加了用户之间的互动和竞争,同时也为答题小程序增添了更多的趣味性和挑战性。那么,如何参与答题小程序的排位PK答题升级赛呢?本文将详细介绍答题小程序的排位PK答题升级赛…