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


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

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

    控制反转

    控制反转(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}
    }
    
    • 1
    • 2
    • 3
    • 4

    对于控制反转来说,如果在NewBookPepo 函数中自行创建相关依赖,使得代码的耦合度比较高,并且难以维护和调试。

    为了解决这个问题,大佬们就开始想办法,在还华中尽可能的使用控制反转和依赖注入将程序解耦合开,从而写出灵活,并易于测试的程序。

    为什么需要依赖注入工具

    在小型应用程序中,我们可以自行创建依赖并手动注入。但是在一个大型应用程序中,手动去实现所有依赖的创建和注入就会比较繁琐。

    为了方便管理业务,和技术分层,会在实际中划分住不同的代码层。其中MVC就是一个非常常见的业务思想。

    例如:
    HTTP服务中:
    这中模型是最为常见的的模型。
    在这里插入图片描述

    服务需要有一个配置,指定工作模式、连接的数据库和监听端口等信息。(conf

    目录: conf/conf.go

    // conf/conf.go
    
    // NewDefaultConfig 返回默认配置,不需要依赖
    func NewDefaultConfig() *Config {...}
    
    • 1
    • 2
    • 3
    • 4

    这里定义了一个默认配置,当然后续可以支持从配置文件或环境变量读取配置信息

    在程序的data层,需要定义一个连接数据库的函数,它依赖上面定义的Config并返回一个*gorm.DB(这里使用gorm连接数据库)

    目录:data/data.go

    // data/data.go
    
    // NewDB 返回数据库连接对象
    func NewDB(cfg *conf.Config) (*gorm.DB, error) {...}
    
    • 1
    • 2
    • 3
    • 4

    同时定义一个BookRepo,它有一些数据操作相关的方法。它的构造函数NewBookRepo依赖*gorm.DB,并返回一个*BookRepo

    目录:data/data.go

    // data/data.go
    
    type BookRepo struct {
    	db *gorm.DB
    }
    
    func NewBookRepo(db *gorm.DB) *BookRepo {...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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 {...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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 {...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    目录:main.go

    // main.go
    
    type Server interface {
    	Run()
    }
    
    type App struct {
    	server Server
    }
    
    func newApp(server Server) *App {...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    由于在程序中定义了大量需要依赖注入的构造函数,程序的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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    所有依赖的创建和顺序都需要手动维护。

    故我们就需要一个工具来解决这个问题。

    wire的概念

    Go社区中有很多依赖注入框架。比如:UberdigFacebookinject都使用反射来做运行时依赖注入

    Wire 是一个的 Google 开源的依赖注入工具,通过自动生成代码的方式在编译期完成依赖注入。

    wire中有两个核心概念:提供者(provider)注入器(injector)

    提供者(provider)

    提供者函数可以分组为提供者函数集(provider set)。使用wire.NewSet 函数可以将多个提供者函数添加到一个集合中。如果经常同时使用多个提供者函数,这非常有用。

    package demo
    
    import (
        // ...
        "github.com/google/wire"
    )
    
    // ...
    
    var ProviderSet = wire.NewSet(NewX, NewY, NewZ)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    package demo
    
    import (
        // ...
        "example.com/some/other/pkg"
    )
    var MegaSet = wire.NewSet(ProviderSet, pkg.OtherSet)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    1. 可以产生值的普通函数
    type X struct {
        Value int
    }
    
    // NewX 返回一个X对象
    func NewX() X {
      return X{Value: 7}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 可以使用参数指定依赖项
    type Y struct {
        Value int
    }
    
    // NewY 返回一个Y对象,需要传入一个X对象作为依赖。
    func NewY(x X) Y {
      return Y{Value: x.Value+1}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    从生成的内容可以看出,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,
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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"),
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这个wire会生成一个类似于:

    func injectFooBar() FooBar {
        foo := ProvideFoo()
        bar := ProvideBar()
        fooBar := FooBar{
            MyFoo: foo,
            MyBar: bar,
        }
        return fooBar
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    wire.Struct的第一个参数是指向所需结构体类型的指针,随后的参数是要注入的字段的名称。可以使用一个特殊的字符串“*”作为快捷方式,告诉注入器注入结构体的所有字段。

    指针结构体传入的中

    对于生成的结构体类型Swire.struct同时提供S*S

    注入MyFoo字段
    var Set = wire.NewSet(
        ProvideFoo,
        wire.Struct(new(FooBar), "MyFoo"),
    )
    
    • 1
    • 2
    • 3
    • 4

    1.生成的类似于:

    func injectFooBar() FooBar {
        foo := ProvideFoo()
        fooBar := FooBar{
            MyFoo: foo,
        }
        return fooBar
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.生成的类似于:

    func injectFooBar() *FooBar {
        foo := ProvideFoo()
        fooBar := &FooBar{
            MyFoo: foo,
        }
        return fooBar
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    重要

    有时防止结构体的某些字段被注入器填充很有必要,尤其是在将*传递给wire.Struct的时候。你可以用wire:"-"标记字段,使wire忽略这些字段。

    type Foo struct {
        mu sync.Mutex `wire:"-"`
        Bar Bar
    }
    
    • 1
    • 2
    • 3
    • 4

    使用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{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    生成的注入器:

    func injectFoo() Foo {
        foo := _wireFooValue
        return foo
    }
    
    var (
        _wireFooValue = Foo{X: 42}
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    值得注意的是,表达式将被复制到注入器的包中。

    对变量的引用将在注入器包的初始化过程中进行计算。如果表达式调用任何函数从任何通道接收任何函数,wire 将会报错。

    接口值

    对于接口值,使用 InterfaceValue。

    func injectReader() io.Reader {
        wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
        return nil
    }
    
    • 1
    • 2
    • 3
    • 4

    使用结构的字段作为提供者

    用户想要的提供程序是结构的某些字段,如果发现自己在下面的示例中编写了一个类似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 ""
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    可以使用wire.FieldsOf直接使用结构体的字段,而无需编写一个类似getS的函数:

    func injectedMessage() string {
        wire.Build(
            provideFoo,
            wire.FieldsOf(new(Foo), "S"),
        )
        return ""
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    生成为:

    func injectedMessage() string {
        foo := provideFoo()
        string2 := foo.S
        return string2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以根据需要将任意多的字段名称添加到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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    注意
    cleanup函数的签名必须是func(),并且保证在提供者的任何输入的cleanup函数之前调用。

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

  • 相关阅读:
    leetcode 55. 跳跃游戏
    面试经典 150 题 2 —(滑动窗口)— 3. 无重复字符的最长子串
    java---网络初始
    计算机毕业设计选题推荐-基于数据可视化的智慧社区内网平台-Python项目实战
    高教社杯数模竞赛特辑论文篇-2023年C题:基于历史数据的蔬菜类商品定价与补货决策模型(附获奖论文及R语言和Python代码实现)(中)
    我要写整个中文互联网界最牛逼的JVM系列教程 | 「JVM与Java体系架构」章节:Java及JVM历史上的重大事件
    shell脚本之数组
    【机器学习】面试题:LSTM长短期记忆网络的理解?LSTM是怎么解决梯度消失的问题的?还有哪些其它的解决梯度消失或梯度爆炸的方法?
    docker启动,解决jenkins内存占用过高
    玩转 SpringBoot 监控统计(SQL监控、慢SQL记录、Spring监控、去广告)
  • 原文地址:https://blog.csdn.net/Cheer_RIO/article/details/134242060