GoLong的学习之路(番外)如何使用依赖注入工具:wire

news2024/12/28 6:56:21

我为什么要直接写番外呢?其原因很简单。项目中会使用,其实在这里大家就可以写一些项目来了。

依赖注入的工具本质思想其实都大差不差。无非控制反转和依赖注入。

文章目录

  • 控制反转
    • 为什么需要依赖注入工具
  • 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.go

type BookRepo struct {
	db *gorm.DB
}

func NewBookRepo(db *gorm.DB) *BookRepo {...}

Service层位于data层Server层的中间,它负责实现对外服务。其中构造函数 NewBookService 依赖ConfigBookRepo
目录:service/service.go

// service/service.go

type BookService struct {
	config *conf.Config
	repo   *data.BookRepo
}

func NewBookService(cfg *conf.Config, repo *data.BookRepo) *BookService {...}

server层又有一个NewServer构造函数,它依赖外部传入ConfigBookService
目录:server/server.go

// server/server.go

type Server struct {
	config  *conf.Config
	service *service.BookService
}

func NewServer(cfg *conf.Config, srv *service.BookService) *Server {...}

main.go文件中又依赖Server创建一个app

目录:main.go

// main.go

type Server interface {
	Run()
}

type App struct {
	server Server
}

func newApp(server Server) *App {...}

由于在程序中定义了大量需要依赖注入的构造函数,程序的main函数中会出现以下情形。
目录:main.go

// main.go

func 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 demo

import (
    // ...
    "github.com/google/wire"
)

// ...

var ProviderSet = wire.NewSet(NewX, NewY, NewZ)

而这个集合也可以作为提供者函数。

package demo

import (
    // ...
    "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 main

import (
    "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 !wireinject

package main

import (
	"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 string

func (b *MyFooer) Foo() string {
    return string(*b)
}

func provideMyFooer() *MyFooer {
    b := new(MyFooer)
    *b = "Hello, World!"
    return b
}

type Bar string

func 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 int

func ProvideFoo() Foo {/* ... */}

func ProvideBar() Bar {/* ... */}

type FooBar struct {
    MyFoo Foo
    MyBar 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 := _wireFooValue
    return 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 string
    N int
    F 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.S
    return 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.coloradmin.cn/o/1179885.html

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

相关文章

易云维®工厂能耗管理平台系统方案,保证运营质量,推动广东制造企业节能减排

我国《关于完整准确全面贯彻新发展理念推进碳达峰碳中和工作的实施意见》出台,提出了推进碳达峰碳中和工作的总体目标。到2025年,广东具备条件的地区、行业和企业率先实现碳达峰,为全省实现碳达峰、碳中和奠定坚实基础;2030年前实…

51单片机+SIM800C(GSM模块)实现短信发送功能

一、前言 本项目利用51单片机和SIM800C GSM模块实现短信发送功能。短信作为一种广泛应用的通信方式,在许多领域具有重要的作用,如物联网、安防系统、远程监控等。通过将51单片机与SIM800C GSM模块相结合,可以实现在各种应用场景下的短信通信…

美国光量子计算解决方案公司QCI正式开启量子计算商业化道路!

​(图片来源:网络) Quantum Computing Inc(QCI)是一家率先实现上市的全栈式光量子计算解决方案公司,近日,美国量子计算公司QCI(纳斯达克代码: QUBT)宣布,其在…

Xilinx DDR3 MIG系列——内存基本概念及原理

本节目录 一、内存简介 (1)内存基本存储原理 (2)内存频率 (3)DDR数据预取技术(Prefetch) (4)DDR3工作流程 (5)DDR3控制器的特点 二、内存基本参数 (1)物理Bank (2)逻辑Bank (3)内存芯片容量 (4)行激活命令—tRCD (5)列选通—CL (6)写入延迟—tDQSS (7)行预充电有效周期—tRP (8…

同样是BGA扇出,为什么别人设计出来的性能就是比你好!

高速先生成员--黄刚 高速先生经常会说一句话,那就是对于信号质量的优化是无极限的,这里说的优化,其实说的就是PCB的设计优化。首先肯定的是,不同的设计工程师去做同样一块PCB板的设计,做出来的肯定都不会完全一样。那不…

DVWA靶场SQL注入

本次注入的是DVWA靶场的SQL injection 1.判断是字符型注入还是数字型注入,构造SQL语句 1 and 12 由此可以判断出为字符型注入 2.考虑闭合方式,先随便丢一个单引号试试看看报错提示 You have an error in your SQL syntax; check the manual that cor…

C语言C位出道心法(二):结构体

C语言C位出道心法(一):基础语法 C语言C位出道心法(二):结构体 一:C语言结构体认知 忙着去耍帅,后期补充完整...........

python简易定时调度

在python中,一般定时调度会使用apscheduler,更基础的我们会使用sched包来实时调度。有时候也是会使用time.sleep的方式来配合使用。在实际的生产中,单纯使用time.sleep会出现睡眼之后程序无法被唤醒的情况,所以后来会尽量的避免使用这种方式。…

Django初窥门径-项目初始化

环境准备 切换pypi源 运行下面的脚本将pypi源切换为阿里云镜像,避免安装python库的过程中出现网络问题 #!/bin/bash# 定义配置内容 config_content"[global] index-url http://mirrors.aliyun.com/pypi/simple/[install] trusted-hostmirrors.aliyun.com &…

VSCode修改主题为Eclipse 绿色护眼模式

前言 从参加开发以来,一直使用eclipse进行开发,基本官方出新版本,我都会更新。后来出来很多其他的IDE工具,我也尝试了,但他们的主题都把我劝退了,黑色主题是谁想出来?😂 字体小的时…

马斯克发布大模型Grok;主流AI创意生成工具图谱;Runway视频大赛获奖作品解析;DALL-E 3图像混合操作;42章经播客推荐 | ShowMeAI日报

👀日报&周刊合集 | 🎡生产力工具与行业应用大全 | 🧡 点赞关注评论拜托啦! 👀 马斯克「xAI」发布首个AI大模型「Grok」 https://grok.x.ai 11月5日,马斯克旗下人工智能公司 xAI 发布了首款 AI 聊天产品…

(附源码)基于微服务架构的餐饮系统的设计与实现-计算机毕设 86393

基于微服务架构的餐饮系统的设计与实现 摘 要 近年来,我国经济和社会发展迅速,人们物质生活水平日渐提高,餐饮行业更是发展迅速,人们对于餐饮行业的认识和要求也越来越高。传统形式的餐饮行业都是以人为本,管理起来需要很多人力、物力、财力,既不方便管理者的管理,也不方便顾…

YB506A是一款锂电池充、放电管理专用芯片,集成锂电池充电管理和降压DCDC电路

YB506A 锂电转可充电AA/AAA电池专用SOC芯片 概述: YB506A是一款锂电池充、放电管理专用芯片,集成锂电池充电管理和降压DCDC电路。充电过程满足锂电池三段式滑流/恒流/恒压充电规范,YB506A内部的线性充电电路采用了恒流可配置模式&#xff0…

基于食肉植物算法的无人机航迹规划-附代码

基于食肉植物算法的无人机航迹规划 文章目录 基于食肉植物算法的无人机航迹规划1.食肉植物搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要:本文主要介绍利用食肉植物算法来优化无人机航迹规划。 …

Find My挂件|苹果Find My技术与挂件结合,智能防丢,全球定位

挂件是各种场合装饰的一种工艺品。有玉石、金属、塑料、布料等材质的。一般是手机上的挂饰、包袋上的装饰、蓝牙耳机的挂饰、各种物品的装饰都可以用得到。如今最新的挂件加入智能模块可实现物品防丢功能。 在智能化加持下,防丢功能的加入使得人们日益关心物品的去…

2.3.3 交换机的RSTP技术

实验2.3.3 交换机的RSTP技术 一、任务描述二、任务分析三、具体要求四、实验拓扑五、任务实施1.交换机的基本配置。2.开启交换机的STP。3.配置SW3A和SW3B上STP的优先级。将SW3A配置为根交换机,SW3B配置为备用根交换机。4.配置SW2A和SW3B的边缘接口 六、任务验收七、…

.NET MVC 修改项目URL为IP

先从隐藏目录里找到applicationhost.config 如果没有找到这.vs的目录的话就先创建虚拟路径即可然后在下图里插入一行本机IP的数据,注意只能插入不能直接改上面的localhost否则就无法创建虚拟目录了复制上面一行,把localhost改为你的本机IP 配置文件改完…

Fastjson 1.2.47 RCE漏洞复现

一、漏洞特征 Fastjson提供了autotype功能,允许用户在反序列化数据中通过“type”指定反序列化的类型,Fastjson自定义的反序列化机制时会调用指定类中的setter方法及部分getter方法,那么当组件开启了autotype功能并且反序列化不可信数据时&am…

11.7移位寄存器,计数器,时序分析,状态机,verliog实现

D触发器实现二选1 4位移位寄存器 移位寄存器&#xff0c;可以将寄存的Q信号逐位输出 N位寄存器 递增计数器 带并行载入端递增 这里载入的时候&#xff0c;省略掉的ELSE&#xff0c;自动操作就是锁存原状态&#xff0c;即Q<Q 并行&#xff0c;递减 这个没有复位端 递增递减…

节省服务器资源、实现双向数据传输——深度解析WebSocket协议

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、W…