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中的两个概念:Provider
和Injector
:
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 configimport ( "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 dbimport ( "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 mainimport ( "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 wireinjectpackage mainimport ( "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>wirewire: 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 !wireinjectpackage mainimport ( "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
文件须要放在创立指标对象的中央,比方咱们Config
和DB
对象最终是为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.go
和wire_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 dbimport "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 mainimport ( "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>wirewire: 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 failedwire: 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.go
的Provider
申明,减少db.*dao
和db.Dao
的接口绑定关系:
var Provider = wire.NewSet(New, NewDao, wire.Bind(new(Dao), new(*dao)))
wire.Bind()
办法第一个参数为interface{}
,第二个参数为实现
。
此时再执行wire
命令就能够胜利了!
结尾
wire
工具还有很多玩法,然而就笔者集体工作教训而言,把握本文介绍到的常识曾经可能胜任绝大部分场景了!