乐趣区

关于golang:golang依赖注入工具wire指南

wire 与依赖注入

Wire 是一个的 Golang 依赖注入工具,通过主动生成代码的形式在 编译期 实现依赖注入,Java 体系中最闻名的 Spring 框架采纳 运行时 注入,集体认为这是 wire 和其余依赖注入最大的不同之处。

依赖注入 (Dependency Injection) 也称作管制反转(Inversion of Control),集体给管制反转下的定义如下:

以后对象须要的依赖对象由内部提供(通常是 IoC 容器),内部负责依赖对象的结构等操作,以后对象只负责调用,而不关怀依赖对象的结构。即依赖对象的控制权交给了 IoC 容器。

上面给出一个管制反转的示例,比方咱们通过配置去创立一个数据库连贯:

// 连贯配置
type DatabaseConfig struct {Dsn string}

func NewDB(config *DatabaseConfig)(*sql.DB, error) {db,err := sql.Open("mysql", config.Dsn)
    if err != nil {return nil, err}
    // ...
}

fun NewConfig()(*DatabaseConfig,error) {
    // 读取配置文件
    fp, err := os.Open("config.json")
    if err != nil {return nil,err}
    defer fp.Close()
    // 解析为 Json
    var config DatabaseConfig
    if err:=json.NewDecoder(fp).Decode(&config);err!=nil {return nil,err}
    return &config, nil
}

func InitDatabase() {cfg, err:=NewConfig()
    if err!=nil {log.Fatal(err)
    }
    db,err:=NewDB(cfg)
    if err!=nil {log.Fatail(err)
    }
    // db 对象结构结束
}

数据库配置怎么来的,NewDB办法并不关怀 (示例代码采纳的是NewConfig 提供的 JSON 配置对象),NewDB只负责创立 DB 对象并返回,和配置形式并没有耦合,所以即便换成配置核心或者其余形式来提供配置,NewDB代码也无需更改,这就是管制反转的魔力!

来看一个背面例子,也就是管制正转:

以后对象须要的依赖由本人创立,即依赖对象的控制权在以后对象本人手里。

type DatabaseConfig struct {Dsn string}

func NewDB()(*sql.DB, error) {
    // 读取配置文件
    fp, err := os.Open("config.json")
    if err != nil {return nil,err}
    defer fp.Close()
    // 解析为 Json
    var config DatabaseConfig
    if err:=json.NewDecoder(fp).Decode(&config);err!=nil {return nil,err}
    // 初始化数据库连贯
    db,err = sql.Open("mysql", config.Dsn)
    if err != nil {return}
    // ...
}

在管制正转模式下,NewDB办法须要本人实现配置对象的创立工作,在示例中须要读取 Json 配置文件,这是 强耦合 的代码,一旦配置文件的格局不是 Json,NewDB办法将返回谬误。

依赖注入诚然好用,然而像方才的例子中去手动治理依赖关系是相当简单也是相当苦楚的一件事,因而在接下来的内容中会重点介绍 golang 的依赖注入工具——wire。

上手应用

通过 go get github.com/google/wire/cmd/wire 装置好 wire 命令行工具即可。

在正式开始之前须要介绍一下 wire 中的两个概念:ProviderInjector

  • Provider:负责创建对象的办法,比方上文中 管制反转示例 NewDB(提供 DB 对象)和 NewConfig(提供 DatabaseConfig 对象) 办法。
  • Injector:负责依据对象的依赖,顺次结构依赖对象,最终结构目标对象的办法,比方上文中 管制反转示例 InitDatabase办法。

当初咱们通过 wire 来实现一个简略的我的项目。我的项目构造如下:

|--cmd
    |--main.go
    |--wire.go
|--config
    |--app.json
|--internal
    |--config
        |--config.go
    |--db
        |--db.go

config/app.json

{
  "database": {"dsn": "root:root@tcp(localhost:3306)/test"
  }
}

internal/config/config.go

package config

import (
    "encoding/json"
    "github.com/google/wire"
    "os"
)

var Provider = wire.NewSet(New) // 将 New 办法申明为 Provider,示意 New 办法能够创立一个被他人依赖的对象, 也就是 Config 对象

type Config struct {Database database `json:"database"`}

type database struct {Dsn string `json:"dsn"`}

func New() (*Config, error) {fp, err := os.Open("config/app.json")
    if err != nil {return nil, err}
    defer fp.Close()
    var cfg Config
    if err := json.NewDecoder(fp).Decode(&cfg); err != nil {return nil, err}
    return &cfg, nil
}

internal/db/db.go

package db

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "github.com/google/wire"
    "wire-example2/internal/config"
)

var Provider = wire.NewSet(New) // 同理

func New(cfg *config.Config) (db *sql.DB, err error) {db, err = sql.Open("mysql", cfg.Database.Dsn)
    if err != nil {return}
    if err = db.Ping(); err != nil {return}
    return db, nil
}

cmd/main.go

package main

import (
    "database/sql"
    "log"
)

type App struct { // 最终须要的对象
    db *sql.DB
}

func NewApp(db *sql.DB) *App {return &App{db: db}
}

func main() {app, err := InitApp() // 应用 wire 生成的 injector 办法获取 app 对象
    if err != nil {log.Fatal(err)
    }
    var version string
    row := app.db.QueryRow("SELECT VERSION()")
    if err := row.Scan(&version); err != nil {log.Fatal(err)
    }
    log.Println(version)
}

cmd/wire.go

重点文件,也就是实现 Injector 的外围所在:

// +build wireinject

package main

import (
    "github.com/google/wire"
    "wire-example2/internal/config"
    "wire-example2/internal/db"
)

func InitApp() (*App, error) {panic(wire.Build(config.Provider, db.Provider, NewApp)) // 调用 wire.Build 办法传入所有的依赖对象以及构建最终对象的函数失去指标对象
}

文件编写结束,进入 cmd 目录执行 wire 命令会失去以下输入:

C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: wire-example2/cmd: wrote C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire_gen.go

表明胜利生成 wire_gen.go 文件,文件内容如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package main

import (
    "wire-example2/internal/config"
    "wire-example2/internal/db"
)

// Injectors from wire.go:

func InitApp() (*App, error) {configConfig, err := config.New()
    if err != nil {return nil, err}
    sqlDB, err := db.New(configConfig)
    if err != nil {return nil, err}
    app := NewApp(sqlDB)
    return app, nil
}

能够看到生成 App 对象的代码曾经主动生成了。

Provider 阐明

通过 NewSet 办法将本包内创建对象的办法申明为 Provider 以供其余对象应用。NewSet能够接管多个参数,比方咱们 db 包内能够创立 Mysql 和 Redis 连贯对象,则能够如下申明:

var Provider = wire.NewSet(NewDB, NewRedis)

func NewDB(config *Config)(*sql.DB,error) {// 创立数据库对象}

func NewRedis(config *Config)(*redis.Client,error) {// 创立 Redis 对象}

wire.go 文件阐明

wire.go文件须要放在创立指标对象的中央,比方咱们 ConfigDB对象最终是为 App 服务的,因而 wire.go 文件须要放在 App 所在的包内。

wire.go 文件名不是固定的,不过大家习惯叫这个文件名。

wire.go的第一行 // +build wireinject 是必须的,含意如下:

只有增加了名称为 ”wireinject” 的 build tag,本文件才会编译,而咱们 go build main.go 的时候通常不会加。因而,该文件不会参加最终编译。

wire.Build(config.Provider, db.Provider, NewApp)通过传入 config 以及 db 对象来创立最终须要的 App 对象

wire_gen.go 文件阐明

该文件由 wire 主动生成,无需手工编辑!!!

//+build !wireinject标签和 wire.go 文件的标签绝对应,含意如下:

编译时只有 未增加“wireinject” 的 build tag,本文件才参加编译。

因而,任意时刻下,wire.gowire_gen.go 只会有一个参加编译。

高级玩法

cleanup 函数

在创立依赖资源时,如果由某个资源创立失败,那么其余资源须要敞开的状况下,能够应用 cleanup 函数来敞开资源。比方咱们给 db.New 办法返回一个 cleanup 函数来敞开数据库连贯,相干代码批改如下(未列出的代码不批改):

internal/db/db.go

func New(cfg *config.Config) (db *sql.DB, cleanup func(), err error) { // 申明第二个返回值
    db, err = sql.Open("mysql", cfg.Database.Dsn)
    if err != nil {return}
    if err = db.Ping(); err != nil {return}
    cleanup = func() { // cleanup 函数中敞开数据库连贯
        db.Close()}
    return db, cleanup, nil
}

cmd/wire.go

func InitApp() (*App, func(), error) { // 申明第二个返回值
    panic(wire.Build(config.Provider, db.Provider, NewApp))
}

cmd/main.go

func main() {app, cleanup, err := InitApp() // 增加第二个参数
    if err != nil {log.Fatal(err)
    }
    defer cleanup() // 提早调用 cleanup 敞开资源
    var version string
    row := app.db.QueryRow("SELECT VERSION()")
    if err := row.Scan(&version); err != nil {log.Fatal(err)
    }
    log.Println(version)
}

从新在 cmd 目录执行 wire 命令,生成的 wire_gen.go 如下:

func InitApp() (*App, func(), error) {configConfig, err := config.New()
    if err != nil {return nil, nil, err}
    sqlDB, cleanup, err := db.New(configConfig)
    if err != nil {return nil, nil, err}
    app := NewApp(sqlDB)
    return app, func() { // 返回了清理函数
        cleanup()}, nil
}

接口绑定

在面向接口编程中,代码依赖的往往是接口,而不是具体的 struct,此时依赖注入相干代码须要做一点小小的批改,持续方才的例子,示例批改如下:

新增internal/db/dao.go

package db

import "database/sql"

type Dao interface { // 接口申明
    Version() (string, error)
}

type dao struct { // 默认实现
    db *sql.DB
}

func (d dao) Version() (string, error) {
    var version string
    row := d.db.QueryRow("SELECT VERSION()")
    if err := row.Scan(&version); err != nil {return "", err}
    return version, nil
}

func NewDao(db *sql.DB) *dao { // 生成 dao 对象的办法
    return &dao{db: db}
}

internal/db/db.go 也须要批改 Provider,减少 NewDao 申明:

var Provider = wire.NewSet(New, NewDao)

cmd/main.go 文件批改:

package main

import (
    "log"
    "wire-example2/internal/db"
)

type App struct {dao db.Dao // 依赖 Dao 接口}

func NewApp(dao db.Dao) *App { // 依赖 Dao 接口
    return &App{dao: dao}
}

func main() {app, cleanup, err := InitApp()
    if err != nil {log.Fatal(err)
    }
    defer cleanup()
    version, err := app.dao.Version() // 调用 Dao 接口办法
    if err != nil {log.Fatal(err)
    }
    log.Println(version)
}

进入 cmd 目录执行 wire 命令,此时会呈现报错:

C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire.go:11:1: inject InitApp: no provider found for wire-example2/internal/db.Dao
        needed by *wire-example2/cmd.App in provider "NewApp" (C:\Users\Administrator\GolandProjects\wire-example2\cmd\main.go:12:6)
wire: wire-example2/cmd: generate failed
wire: at least one generate failure

wire提醒 inject InitApp: no provider found for wire-example2/internal/db.Dao,也就是没找到能提供db.Dao 对象的 Provider,咱们不是提供了默认的db.dao 实现也注册了 Provider 吗?这也是 go 的 OOP 设计奇异之处。

咱们批改一下 internal/db/db.goProvider申明,减少 db.*daodb.Dao的接口绑定关系:

var Provider = wire.NewSet(New, NewDao, wire.Bind(new(Dao), new(*dao)))

wire.Bind()办法第一个参数为 interface{},第二个参数为 实现

此时再执行 wire 命令就能够胜利了!

结尾

wire工具还有很多玩法,然而就笔者集体工作教训而言,把握本文介绍到的常识曾经可能胜任绝大部分场景了!

退出移动版