今天咱們來介紹 Go 語言的一個依賴注入(DI)庫——dig。dig 是 uber 開源的庫。Java 依賴注入的庫有不少,相信即便不是作 Java 開發的童鞋也聽過大名鼎鼎的 Spring。相比龐大的 Spring,dig 很小巧,實現和使用都比較簡潔。mysql
第三方庫須要先安裝,因爲咱們的示例中使用了前面介紹的go-ini和go-flags,這兩個庫也須要安裝:git
$ go get go.uber.org/dig $ go get gopkg.in/ini.v1 $ go get github.com/jessevdk/go-flags
下面看看如何使用:github
package main import ( "fmt" "github.com/jessevdk/go-flags" "go.uber.org/dig" "gopkg.in/ini.v1" ) type Option struct { ConfigFile string `short:"c" long:"config" description:"Name of config file."` } func InitOption() (*Option, error) { var opt Option _, err := flags.Parse(&opt) return &opt, err } func InitConf(opt *Option) (*ini.File, error) { cfg, err := ini.Load(opt.ConfigFile) return cfg, err } func PrintInfo(cfg *ini.File) { fmt.Println("App Name:", cfg.Section("").Key("app_name").String()) fmt.Println("Log Level:", cfg.Section("").Key("log_level").String()) } func main() { container := dig.New() container.Provide(InitOption) container.Provide(InitConf) container.Invoke(PrintInfo) }
在同一目錄下建立配置文件my.ini
:golang
app_name = awesome web log_level = DEBUG [mysql] ip = 127.0.0.1 port = 3306 user = dj password = 123456 database = awesome [redis] ip = 127.0.0.1 port = 6381
運行程序,輸出:web
$ go run main.go -c=my.ini App Name: awesome web Log Level: DEBUG
dig
庫幫助開發者管理這些對象的建立和維護,每種類型的對象會建立且只建立一次。dig
庫使用的通常流程:redis
dig.New
;dig
容器管理的類型建立構造函數,構造函數能夠返回多個值,這些值都會被容器管理;container.Invoke
執行咱們編寫的函數。有時候,建立對象有不少依賴,或者編寫函數時有多個參數依賴。若是將這些依賴都做爲參數傳入,那麼代碼將變得很是難以閱讀:sql
container.Provide(func (arg1 *Arg1, arg2 *Arg2, arg3 *Arg3, ....) { // ... })
dig
支持將全部參數打包進一個對象中,惟一須要的就是將dig.In
內嵌到該類型中:數據庫
type Params { dig.In Arg1 *Arg1 Arg2 *Arg2 Arg3 *Arg3 Arg4 *Arg4 } container.Provide(func (params Params) *Object { // ... })
內嵌了dig.In
以後,dig
會將該類型中的其它字段當作Object
的依賴,建立Object
類型的對象時,會先將依賴的Arg1/Arg2/Arg3/Arg4
建立好。編程
package main import ( "fmt" "log" "github.com/jessevdk/go-flags" "go.uber.org/dig" "gopkg.in/ini.v1" ) type Option struct { ConfigFile string `short:"c" long:"config" description:"Name of config file."` } type RedisConfig struct { IP string Port int DB int } type MySQLConfig struct { IP string Port int User string Password string Database string } type Config struct { dig.In Redis *RedisConfig MySQL *MySQLConfig } func InitOption() (*Option, error) { var opt Option _, err := flags.Parse(&opt) return &opt, err } func InitConfig(opt *Option) (*ini.File, error) { cfg, err := ini.Load(opt.ConfigFile) return cfg, err } func InitRedisConfig(cfg *ini.File) (*RedisConfig, error) { port, err := cfg.Section("redis").Key("port").Int() if err != nil { log.Fatal(err) return nil, err } db, err := cfg.Section("redis").Key("db").Int() if err != nil { log.Fatal(err) return nil, err } return &RedisConfig{ IP: cfg.Section("redis").Key("ip").String(), Port: port, DB: db, }, nil } func InitMySQLConfig(cfg *ini.File) (*MySQLConfig, error) { port, err := cfg.Section("mysql").Key("port").Int() if err != nil { return nil, err } return &MySQLConfig{ IP: cfg.Section("mysql").Key("ip").String(), Port: port, User: cfg.Section("mysql").Key("user").String(), Password: cfg.Section("mysql").Key("password").String(), Database: cfg.Section("mysql").Key("database").String(), }, nil } func PrintInfo(config Config) { fmt.Println("=========== redis section ===========") fmt.Println("redis ip:", config.Redis.IP) fmt.Println("redis port:", config.Redis.Port) fmt.Println("redis db:", config.Redis.DB) fmt.Println("=========== mysql section ===========") fmt.Println("mysql ip:", config.MySQL.IP) fmt.Println("mysql port:", config.MySQL.Port) fmt.Println("mysql user:", config.MySQL.User) fmt.Println("mysql password:", config.MySQL.Password) fmt.Println("mysql db:", config.MySQL.Database) } func main() { container := dig.New() container.Provide(InitOption) container.Provide(InitConfig) container.Provide(InitRedisConfig) container.Provide(InitMySQLConfig) err := container.Invoke(PrintInfo) if err != nil { log.Fatal(err) } }
上面代碼中,類型Config
內嵌了dig.In
,PrintInfo
接受一個Config
類型的參數。調用Invoke
時,dig
自動調用InitRedisConfig
和InitMySQLConfig
,並將生成的*RedisConfig
和*MySQLConfig
「打包」成一個Config
對象傳給PrintInfo
。segmentfault
運行結果:
$ go run main.go -c=my.ini =========== redis section =========== redis ip: 127.0.0.1 redis port: 6381 redis db: 1 =========== mysql section =========== mysql ip: 127.0.0.1 mysql port: 3306 mysql user: dj mysql password: 123456 mysql db: awesome
前面說過,若是構造函數返回多個值,這些不一樣類型的值都會存儲到dig
容器中。參數過多會影響代碼的可讀性和可維護性,返回值過多一樣也是如此。爲此,dig
提供了返回值對象,返回一個包含多個類型對象的對象。返回的類型,必須內嵌dig.Out
:
type Results struct { dig.Out Result1 *Result1 Result2 *Result2 Result3 *Result3 Result4 *Result4 }
dig.Provide(func () (Results, error) { // ... })
咱們把上面的例子稍做修改。將Config
內嵌的dig.In
變爲dig.Out
:
type Config struct { dig.Out Redis *RedisConfig MySQL *MySQLConfig }
提供構造函數InitRedisAndMySQLConfig
同時建立RedisConfig
和MySQLConfig
,經過Config
返回。這樣就不須要將InitRedisConfig
和InitMySQLConfig
加入dig
容器了:
func InitRedisAndMySQLConfig(cfg *ini.File) (Config, error) { var config Config redis, err := InitRedisConfig(cfg) if err != nil { return config, err } mysql, err := InitMySQLConfig(cfg) if err != nil { return config, err } config.Redis = redis config.MySQL = mysql return config, nil } func main() { container := dig.New() container.Provide(InitOption) container.Provide(InitConfig) container.Provide(InitRedisAndMySQLConfig) err := container.Invoke(PrintInfo) if err != nil { log.Fatal(err) } }
PrintInfo
直接依賴RedisConfig
和MySQLConfig
:
func PrintInfo(redis *RedisConfig, mysql *MySQLConfig) { fmt.Println("=========== redis section ===========") fmt.Println("redis ip:", redis.IP) fmt.Println("redis port:", redis.Port) fmt.Println("redis db:", redis.DB) fmt.Println("=========== mysql section ===========") fmt.Println("mysql ip:", mysql.IP) fmt.Println("mysql port:", mysql.Port) fmt.Println("mysql user:", mysql.User) fmt.Println("mysql password:", mysql.Password) fmt.Println("mysql db:", mysql.Database) }
能夠看到InitRedisAndMySQLConfig
返回Config
類型的對象,該類型中的RedisConfig
和MySQLConfig
都被添加到了容器中,PrintInfo
函數可直接使用。
運行結果與以前的例子徹底同樣。
默認狀況下,容器若是找不到對應的依賴,那麼相應的對象沒法建立成功,調用Invoke
時也會返回錯誤。有些依賴不是必須的,dig
也提供了一種方式將依賴設置爲可選的:
type Config struct { dig.In Redis *RedisConfig `optional:"true"` MySQL *MySQLConfig }
經過在字段後添加結構標籤optional:"true"
,咱們將RedisConfig
這個依賴設置爲可選的,容器中RedisConfig
對象也沒關係,這時傳入的Config
中redis
爲 nil,方法能夠正常調用。顯然可選依賴只能在參數對象中使用。
咱們直接註釋掉InitRedisConfig
,而後運行程序:
// 省略部分代碼 func PrintInfo(config Config) { if config.Redis == nil { fmt.Println("no redis config") } } func main() { container := dig.New() container.Provide(InitOption) container.Provide(InitConfig) container.Provide(InitMySQLConfig) container.Invoke(PrintInfo) }
輸出:
$ go run main.go -c=my.ini no redis config
注意,建立失敗和沒有提供構造函數是兩個概念。若是InitRedisConfig
調用失敗了,使用Invoke
執行PrintInfo
仍是會報錯的。
前面咱們說過,dig
默認只會爲每種類型建立一個對象。若是要建立某個類型的多個對象怎麼辦呢?能夠爲對象命名!
調用容器的Provide
方法時,能夠爲構造函數的返回對象命名,這樣同一個類型就能夠有多個對象了。
type User struct { Name string Age int } func NewUser(name string, age int) func() *User{} { return func() *User { return &User{name, age} } } container.Provide(NewUser("dj", 18), dig.Name("dj")) container.Provide(NewUser("dj2", 18), dig.Name("dj2"))
也能夠在結果對象中經過結構標籤指定:
type UserResults struct { dig.Out User1 *User `name:"dj"` User2 *User `name:"dj2"` }
而後在參數對象中經過名字指定使用哪一個對象:
type UserParams struct { dig.In User1 *User `name:"dj"` User2 *User `name:"dj2"` }
完整代碼:
package main import ( "fmt" "go.uber.org/dig" ) type User struct { Name string Age int } func NewUser(name string, age int) func() *User { return func() *User { return &User{name, age} } } type UserParams struct { dig.In User1 *User `name:"dj"` User2 *User `name:"dj2"` } func PrintInfo(params UserParams) error { fmt.Println("User 1 ===========") fmt.Println("Name:", params.User1.Name) fmt.Println("Age:", params.User1.Age) fmt.Println("User 2 ===========") fmt.Println("Name:", params.User2.Name) fmt.Println("Age:", params.User2.Age) return nil } func main() { container := dig.New() container.Provide(NewUser("dj", 18), dig.Name("dj")) container.Provide(NewUser("dj2", 18), dig.Name("dj2")) container.Invoke(PrintInfo) }
程序運行結果:
$ go run main.go User 1 =========== Name: dj Age: 18 User 2 =========== Name: dj2 Age: 18
須要注意的時候,NewUser
返回的是一個函數,由dig
在須要的時候調用。
組能夠將相同類型的對象放到一個切片中,能夠直接使用這個切片。組的定義與上面名字定義相似。能夠經過爲Provide
提供額外的參數:
container.Provide(NewUser("dj", 18), dig.Group("user")) container.Provide(NewUser("dj2", 18), dig.Group("user"))
也能夠在結果對象中添加結構標籤group:"user"
。
而後咱們定義一個參數對象,經過指定一樣的結構標籤來使用這個切片:
type UserParams struct { dig.In Users []User `group:"user"` } func Info(params UserParams) error { for _, u := range params.Users { fmt.Println(u.Name, u.Age) } return nil } container.Invoke(Info)
最後咱們經過一個完整的例子演示組的使用,咱們將建立一個 HTTP 服務器:
package main import ( "fmt" "net/http" "go.uber.org/dig" ) type Handler struct { Greeting string Path string } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s from %s", h.Greeting, h.Path) } func NewHello1Handler() HandlerResult { return HandlerResult{ Handler: Handler{ Path: "/hello1", Greeting: "welcome", }, } } func NewHello2Handler() HandlerResult { return HandlerResult{ Handler: Handler{ Path: "/hello2", Greeting: "😄", }, } } type HandlerResult struct { dig.Out Handler Handler `group:"server"` } type HandlerParams struct { dig.In Handlers []Handler `group:"server"` } func RunServer(params HandlerParams) error { mux := http.NewServeMux() for _, h := range params.Handlers { mux.Handle(h.Path, h) } server := &http.Server{ Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { return err } return nil } func main() { container := dig.New() container.Provide(NewHello1Handler) container.Provide(NewHello2Handler) container.Invoke(RunServer) }
咱們建立了兩個處理器,添加到server
組中,在RunServer
函數中建立 HTTP 服務器,將這些處理器註冊到服務器中。
運行程序,在瀏覽器中輸入localhost:8080/hello1
和localhost:8080/hello2
看看。關於 Go Web 編程相關的知識,能夠看看我寫的 Go Web 編程系列文章:
使用dig
過程當中會遇到一些錯誤,咱們來看看常見的錯誤。
Invoke
方法在如下幾種狀況下會返回一個error
:
Invoke
執行的函數返回error
,該錯誤也會被傳給調用者。這兩種狀況,咱們均可以判斷Invoke
的返回值來查找緣由。
本文介紹了dig
庫,它適用於解決循環依賴的對象建立問題。同時也有利於將關注點分離,咱們不須要將各類對象傳來傳去,只須要將構造函數交給dig
容器,而後經過Invoke
直接使用依賴便可,連判空邏輯均可以省略了!
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~
本文由博客一文多發平臺 OpenWrite 發佈!