1.控制反转与依赖注入

Wire 是一个的Golang依赖注入工具,通过自动生成代码的方式在编译期完成依赖注入,Java体系中最出名的Spring框架采用运行时注入,个人认为这是wire和其他依赖注入最大的不同之处。

控制反转(Inversion of Control简称IoC),定义如下:

一个对象通常会直接创建其依赖的对象,从而导致紧耦合。控制反转则是将对象的创建和管理职责转移出去,使得组件不再负责自己需要的依赖。

依赖注入(Dependency Injection简称DI),定义如下:

依赖注入是控制反转的一种实现方式,而控制反转是一个更广泛的概念。在实际应用中,依赖注入是最常用的实现控制反转的方法之一。

1.1控制反转示例

控制反转的示例,比如通过配置去创建一个数据库连接:

// 连接配置
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代码也无需更改,这就是控制反转。

1.2控制正转示例

当前对象需要的依赖由自己创建,即依赖对象的控制权在当前对象自己手里。

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
    }
    // ...
}
在控制正转模式(Control Forward)下,NewDB方法需要自己实现配置对象的创建工作,在示例中需要读取Json配置文件,这是强耦合的代码,一旦配置文件的格式不是Json,NewDB方法将返回错误。

2.Wire

依赖注入固然好用,但是像刚才的例子中去手动管理依赖关系是相当复杂也是相当痛苦的一件事,因此在接下来的内容中会重点介绍golang的依赖注入工具——Wire。

安装好wire命令行工具

go get github.com/google/wire/cmd/wire
go install github.com/google/wire/cmd/wire

2.1wire中的两个概念(Provider和Injector):

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

通过wire来实现一个简单的项目。项目结构如下:

 fly@flydeMac-Pro  /Volumes/data/go/src/fly  tree ./
./
├── cmd
│   ├── main.go //3
│   ├── wire.go //4
│   └── wire_gen.go //wire命令自动生成
├── config
│   └── config.json //0
├── go.mod
├── go.sum
└── internal
    ├── config
    │   └── config.go //1
    └── data
        └── db.go //2

config/config.json

{
  "database": {
    "dsn": "root:123456@tcp(localhost:3306)/test"
  }
}
internal/config/config.go
package config

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

// 3.将New方法声明为Provider,表示NewConfig方法可以创建一个被别人依赖的对象,也就是Config对象
var Provider = wire.NewSet(NewConfig)

// 1.创建两个嵌套对象,用于反序列化json
type Config struct {
	Database database `json:"database"`
}

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

// 2.工厂方法,用于读取json配置文件并反序列化为Config结构体
func NewConfig() (conf *Config, err error) {
	openfile, err := os.Open("config/config.json")
	if err != nil {
		return nil, err
	}

	defer openfile.Close()

	if err = json.NewDecoder(openfile).Decode(&conf); err != nil {
		return nil, err
	}

	return conf, nil
}

internal/data/db.go

package data

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

// 5.同3
var Provider = wire.NewSet(NewDb)

// 4.构造db连接工厂函数
func NewDb(cfg *config.Config) (db *sql.DB, err error) {
	dbConn, err := sql.Open("mysql", cfg.Database.Dsn)
	if err != nil {
		return nil, err
	}

	if err = dbConn.Ping(); err != nil {
		return nil, err
	}

	return dbConn, nil
}
cmd/main.go
package main

import (
	"database/sql"
	"log"
)

// 6. 添加最终需要的对象
type App struct {
	db *sql.DB
}

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

// 8.生成完Injector后编写调用
func main() {
	app, err := InitApp() // 使用wire生成的injector方法获取app对象
	if err != nil {
		panic(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
//go:build wireinject

package main

import (
	"fly/internal/config"
	"fly/internal/data"
	"github.com/google/wire"
)

// 7.调用wire.Build方法传入所有的依赖对象以及构建最终对象的函数得到目标对象
func InitApp() (app *App, err error) {
	panic(wire.Build(config.Provider, data.Provider, NewApp))
}

/*
1. panic 调用:panic(wire.Bind(config.Provider, data.Provider, NewApp))
panic 是 Go 语言中的一个内建函数,用于引发运行时异常。调用 panic 会导致程序崩溃,并打印出错误信息。通常 panic 用于处理无法恢复的错误。
这里 panic 的参数是 wire.Bind(config.Provider, data.Provider, NewApp),意味着如果 wire.Bind 产生了错误或某种无法继续的状态,程序将会崩溃。

2. wire.Bind:
wire 是 Go 语言中的一个依赖注入库,常用于通过声明的方式来绑定和注入依赖。
wire.Bind 的作用是将多个组件(通常是“提供者”)绑定到一个最终的构造函数或者接口。在这个例子中,config.Provider 和 data.Provider 被绑定到 NewApp 这个构造函数。

3. config.Provider 和 data.Provider:
这些是 Wire 依赖注入中的 提供者,通常是返回具体实例或服务的函数。
config.Provider 可能提供配置相关的依赖,data.Provider 可能提供与数据存储相关的依赖。

4. NewApp:
NewApp 是一个构造函数,它可能依赖于 config.Provider 和 data.Provider 提供的服务。wire.Bind 会将这些依赖提供给 NewApp,让 NewApp 成为最终的服务或应用实例。
整体理解
这段代码的目的是通过 Wire 依赖注入框架初始化 App 对象,并绑定相关的提供者(config.Provider 和 data.Provider)到 NewApp 构造函数中。如果这个绑定过程出现任何问题,程序会调用 panic,导致应用崩溃并输出错误信息。

这段代码执行了以下操作:
依赖绑定:wire.Bind 将 config.Provider 和 data.Provider 的实例绑定到 NewApp 构造函数中。
错误处理:如果绑定失败,panic 会触发,程序崩溃并报告错误。
*/
编写完毕进入cmd目录执行wire命令:
fly@flydeMac-Pro  /Volumes/data/go/src/fly/cmd  wire
wire: fly/cmd: wrote /Volumes/data/go/src/fly/cmd/wire_gen.go
表明成功生成wire_gen.go文件,文件内容如下:
// Code generated by Wire. DO NOT EDIT.

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

package main

import (
	"fly/internal/config"
	"fly/internal/data"
)

// Injectors from wire.go:

func InitApp() (*App, error) {
	configConfig, err := config.NewConfig()
	if err != nil {
		return nil, err
	}
	db, err := data.NewDb(configConfig)
	if err != nil {
		return nil, err
	}
	app := NewApp(db)
	return app, nil
}
可以看到生成App对象的代码已经自动生成了。

2.2Provider说明

通过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对象

}

2.3wire.go文件说明

wire.go文件需要放在创建目标对象的地方,比如我们Config和DB对象最终是为App服务的,因此wire.go文件需要放在App所在的包内。

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

wire.go的第一行//go:build wireinjectt是必须的,含义如下:

  • go:build wireinject是 Go 语言中的一个构建约束(build constraint),用于指示编译器在构建时只包含特定的代码。这一行通常出现在使用 Wire 依赖注入框架的代码文件中。
  • 构建约束:在 Go 语言中,构建约束用于控制哪些文件在特定条件下被编译。它们通常位于文件的顶部,以 //go:build// +build 的形式出现。
  • //go:build 是 Go 1.17 及以后的版本中引入的新的构建约束语法,旨在替代旧的 // +build 语法。
  • wireinject:
    • wireinject 是一个自定义的构建标签,通常用于标识那些包含 Wire 依赖注入代码的文件。这些文件通常包含依赖注入的生成代码。
    • 只有在使用 wireinject 标签的情况下,Wire 才会生成相应的依赖注入代码。

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

2.4wire_gen.go文件说明

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

  • //+build !wireinject标签和wire.go文件的标签相对应,含义如下:
    • 编译时只有未添加“wireinject"的build tag,本文件才参与编译。

因此,任意时刻下,wire.go和wire_gen.go只会有一个参与编译。

3.Wire的高级用法

3.1cleanup函数

在创建依赖资源时,如果由某个资源创建失败,那么其他资源需要关闭的情况下,可以使用cleanup函数来关闭资源。比如咱们给db.New方法返回一个cleanup函数来关闭数据库连接,相关代码修改如下(未列出的代码不修改): internal/data/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 *App, cleanup func(), err error) {
	panic(wire.Build(config.Provider, data.Provider, NewApp))
}
cmd/main.go
func main() {
	app, cleanup, err := InitApp() // 添加第二个参数
	if err != nil {
		panic(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.NewConfig()
	if err != nil {
		return nil, nil, err
	}
	db, cleanup, err := data.NewDb(configConfig)
	if err != nil {
		return nil, nil, err
	}
	app := NewApp(db)
	return app, func() {
		cleanup()
	}, nil
}

3.2接口绑定

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

新增internal/data/dao.go

package data

import "database/sql"

type Dao interface {
	Version() (string, error)
}

type dao struct {
	db *sql.DB
}

func NewDao(db *sql.DB) *dao { // 生成dao对象的方法
	return &dao{db: 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
}
internal/data/db.go也需要修改Provider,增加NewDao声明:
var Provider = wire.NewSet(NewDb, NewDao)

cmd/main.go文件修改:

package main

import (
	"fly/internal/data"
	"log"
)

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

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

func main() {
	app, cleanup, err := InitApp() // 使用wire生成的injector方法获取app对象
	if err != nil {
		panic(err)
	}

	defer cleanup()

	version, err := app.dao.Version() // 调用Dao接口方法
	if err != nil {
		log.Fatal(err)
	}
	log.Println(version)
}

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

 fly@flydeMac-Pro  /Volumes/data/go/src/fly/cmd  wire
wire: /Volumes/data/go/src/fly/cmd/wire.go:12:1: inject InitApp: no provider found for fly/internal/data.Dao
        needed by *fly/cmd.App in provider "NewApp" (/Volumes/data/go/src/fly/cmd/main.go:13:6)
wire: fly/cmd: generate failed
wire: at least one generate failure

#这表明在 NewApp 的提供者中,Wire 需要一个 Dao 接口的实现,但没有找到相应的提供者。

  • 在上面data层修改的代码片段中,定义了一个 Dao 接口和一个实现该接口的 dao 结构体,以及一个用于创建 dao 实例的构造函数 NewDao。但是,没有在 Wire 的提供者集合中注册 Dao 接口。
  • 解决这个问题,需要确保在 Wire 的提供者集合中注册 Dao 接口的实现。您可以通过以下步骤进行修复:
    • 更新提供者集合:在 Provider 中添加 Dao 的提供者。
var Provider = wire.NewSet(NewDb, NewDao, wire.Bind(new(Dao), new(*dao)))
  • wire.Bind(new(Dao), new(*dao)):这行代码告诉 Wire,dao 结构体实现了 Dao 接口。这样,Wire 就知道如何提供 Dao 接口的实例;wire.Bind()方法第一个参数为interface{},第二个参数为实现。

再次执行wire命令wire_gen.go生成如下

func InitApp() (*App, func(), error) {
	configConfig, err := config.NewConfig()
	if err != nil {
		return nil, nil, err
	}
	db, cleanup, err := data.NewDb(configConfig)
	if err != nil {
		return nil, nil, err
	}
	dao := data.NewDao(db)
	app := NewApp(dao)
	return app, func() {
		cleanup()
	}, nil
}

启动mysql测试一下

fly@flydeMac-Pro  ~  docker pull mysql:8.0
8.0: Pulling from library/mysql
2c0a233485c3: Pull complete 
b746eccf8a0b: Pull complete 
570d30cf82c5: Pull complete 
c7d84c48f09d: Pull complete 
e9ecf1ccdd2a: Pull complete 
6331406986f7: Pull complete 
f93598758d10: Pull complete 
6c136cb242f2: Pull complete 
d255d476cd34: Pull complete 
dbfe60d9fe24: Pull complete 
9cb9659be67b: Pull complete 
Digest: sha256:d58ac93387f644e4e040c636b8f50494e78e5afc27ca0a87348b2f577da2b7ff
Status: Downloaded newer image for mysql:8.0
docker.io/library/mysql:8.0
 fly@flydeMac-Pro  ~             
 fly@flydeMac-Pro  ~  docker run -d -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
--name mysql \
mysql:8.0
e8ae78dc53e7644d274d1ff3812eb5dd5ccfee8fe2329ba53b5db6f6269d4700
 fly@flydeMac-Pro  ~  docker ps
CONTAINER ID   IMAGE       COMMAND                   CREATED         STATUS         PORTS                               NAMES
e8ae78dc53e7   mysql:8.0   "docker-entrypoint.s…"   4 seconds ago   Up 3 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   mysql

CREATE DATABASE `test` /*!40100 DEFAULT CHARACTER 
SET utf8mb4 COLLATE utf8mb4_0900_ai_ci *//*!80016 DEFAULT ENCRYPTION = 'N' */

4.测试

fly@flydeMac-Pro > /Volumes/data/go/src/fly > cd cmd     
fly@flydeMac-Pro > /Volumes/data/go/src/fly/cmd > go build ./       
fly@flydeMac-Pro > /Volumes/data/go/src/fly/cmd > mv cmd myversion
fly@flydeMac-Pro > /Volumes/data/go/src/fly/cmd > chmod +x myversion
fly@flydeMac-Pro > /Volumes/data/go/src/fly/cmd > mv myversion ../  
fly@flydeMac-Pro > /Volumes/data/go/src/fly/cmd > cd ../            
fly@flydeMac-Pro > /Volumes/data/go/src/fly  ./myversion 
2024/12/19 23:59:08 8.0.40

Last updated 20 Dec 2024, 10:40 +0800 . history