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.goconfig/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文件需要放在創建目標對象的地方,比如我們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 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 failurewire提示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工具還有很多玩法,但是就筆者個人工作經驗而言,掌握本文介紹到的知識已經能夠勝任絕大部分場景了!