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.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.mfbz.cn/a/124610.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[Leetcode] 0119. 杨辉三角 II

119. 杨辉三角 II 题目描述 给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例 1: 输入: rowIndex 3 输出: [1,3,3,1]示例 2: 输入: rowIndex 0 输出: [1]示例 3: 输入: rowIn…

【OpenCV实现图像:图像处理技巧之空间滤波】

文章目录 概要导入库空间过滤器模板展示效果分析与总结 概要 空间滤波器是数字图像处理中的基本工具之一。它通过在图像的每个像素位置上应用一个特定的滤波模板,根据该位置周围的相邻像素值进行加权操作,从而修改该像素的值。这种加权操作能够突出或模…

igbt好坏判断方法有哪些?万用表怎么测试igbt的好坏?

什么是IGBT? IGBT即绝缘栅双极型晶体管,是一种复合全控型电压驱动式功率半导体器件,是电力控制和电力转换的核心器件,在高电压和高电流的光伏逆变器、储能装置和新能源汽车等领域被广泛应用。IGBT具有高输入阻抗,低导通压降&…

环形链表I和II

环形链表I 题目描述 给你一个链表的头节点 head ,判断链表中是否有环。如果链表中存在环,则返回true。否则,返回false 。 解题思路 采用快慢指针的思想,创建fast和slow一快一慢指针,slow一次走一步,fas…

golang正则获取中括号中的内容

reg : regexp.MustCompile("【(.*?)】") //userInfo姓名:【AAA姓名】证件类型:【BBB身份证】证件号码:【122456789458】tempData reg.FindAllStringSubmatch(userInfo, -1)for k, v : range tempData {if k 0 {tempReleaseUser.Name v[1]//AAA姓名} else if k 1…

OpenCV(opencv_apps)在ROS中的视频图像的应用(重点讲解哈里斯角点的检测)

1、引言 通过opencv_apps,你可以在ROS中以最简单的方式运行OpenCV提供的许多功能,也就是说,运行一个与功能相对应的launch启动文件,就可以跳过为OpenCV的许多功能编写OpenCV应用程序代码,非常的方便。 对于想熟悉每个…

C# Onnx DirectMHP 全范围角度2D多人头部姿势估计

效果 项目 代码 using Microsoft.ML.OnnxRuntime.Tensors; using Microsoft.ML.OnnxRuntime; using OpenCvSharp; using System; using System.Collections.Generic; using System.Windows.Forms; using System.Linq; using System.Numerics;namespace Onnx_Demo {public part…

MySQL进阶_1.逻辑架构和SQL执行流程

文章目录 第一节、逻辑架构剖析1.1、服务器处理客户端请求1.2、Connectors1.3、第1层:连接层1.4、第2层:服务层1.5、 第3层:引擎层1.6、 存储层1.7、小结 第二节、SQL执行流程2.1、查询缓存2.2、解析器2.3、优化器2.4、执行器 第三节、数据库…

redisTemplate不支持zpopmax,解决方案使用reverseRangeWithScore

在redis客户端可以使用zpopmax redisTemplate不支持zpopmax 解决方案 使用reverseRangeWithScore 接下来我们进行测试 我们要返回最大的value,应该是c import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.a…

编译过程 学习 CMake 文档的前置知识

OHHHH,发现自己的基础知识真他妈的是呼呼漏风,,,,,,,,,,, 尴尬得意识到,不仅是英语水平有问题,他码的基础知识…

Linux学习第37天:Linux I2C 驱动实验:哥俩好

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 世界上的很多事物都是成双成对出现的。也包括在驱动开发的过程中,比如I2C中其实就是数据线和时钟线的相互配合才能完成的。 I2C常用于连接各种外设、…

debian/ubuntu/windows配置wiregurad内网服务器(包含掉线自启动)

文章目录 前言一、服务器配置安装wireguard软件生成私钥公钥配置服务器参数配置服务器sysctl参数启动、停止服务端 二、用户端配置安装wireguard软件生成私钥公钥配置客户端参数启动、停止客户端配置服务开机启动 三、服务器添加、删除客户四、配置掉线自启动配置掉线自启动脚本…

【不正经操作】百度深度学习框架paddlepaddle本地运行-Python环境配置笔记

百度深度学习框架PaddlePaddle 百度深度学习框架PaddlePaddle是一个支持深度学习和机器学习的开源框架。它由百度公司于2016年开发并发布,现在已经成为中国最受欢迎的深度学习框架之一,并且在国际上也获得了不少关注。 特点与功能 易于使用 PaddlePa…

二分查找(二分法)

核心代码&#xff08;循环&#xff09;; int f -1;while(left<right){mid(leftright)/2;if(a[mid]key){fmid;break;}if(key<a[mid]) rmid-1;if(key>a[mid]) lmid1;}if(f-1) cout<<"没找到";else cout <<f<<endl;核心代码(递归): int bins…

Unity之NetCode多人网络游戏联机对战教程(6)--NetworkTransform组件

文章目录 前言NetworkTransform是什么玩家移动脚本NetworkTransform字段讲解Synchronizing ("Syncing")ThresholdsLocal spaceInterpolationSlerp PositionUse Quaternion SynchronizationUse Quaternion CompressionUse Half Float PrecisionAuthority modesServer …

0基础学习VR全景平台篇第118篇:利用动作录制器功能避免重复操作 - PS教程

上课&#xff01;全体起立~ 大家好&#xff0c;欢迎观看蛙色官方系列全景摄影课程&#xff01; 嗨&#xff0c;大家好。欢迎收看蛙色VR系列教程之PS利用动作记录器节约补地时间。 大家拍摄在补地的时候&#xff0c;利用插件选择输入输出选项的时候&#xff0c;每次重复操作…

Transformer的最简洁pytorch实现

目录 前言 1. 数据预处理 2. 模型参数 3. Positional Encoding 4. Pad Mask 5. Subsequence Mask 6. ScaledDotProductAttention 7. MultiHeadAttention 8. FeedForward Networks 9. Encoder Layer 10. Encoder 11. Decoder Layer 12. Decoder 13. Transformer 1…

什么是 HwameiStor?

HwameiStor 是一款 Kubernetes 原生的容器附加存储 (CAS) 解决方案&#xff0c;将 HDD、SSD 和 NVMe 磁盘形成本地存储资源池进行统一管理&#xff0c; 使用 CSI 架构提供分布式的本地数据卷服务&#xff0c;为有状态的云原生应用或组件提供数据持久化能力。 具体的功能特性如下…

GoLong的学习之路(二十)进阶,语法之反射(reflect包)

这个是为了接上之前的语法篇的。按照我的学习计划&#xff0c;这里此时应该有一个小项目来做一个统合。但是吧&#xff0c;突然觉得&#xff0c;似乎也没必要。能学go的大部分肯定都是有其他语言的基础的。 接下来说反射 文章目录 反射介绍reflect包TypeOftype name和type kin…

基于springboot的二次元商品销售网站的设计与开发

大家好我是玥沐春风&#xff0c;今天分享一个基于springboot的二次元商品销售网站的设计与开发&#xff0c;项目源码以及部署相关请联系我&#xff0c;文末附上联系信息 。 开发工具及技术 2.3.1 Spring Boot框架 SpringBoot是一个全新的开源的轻量级框架。简化了Spring应用的…