在 Golang 中玩轉依賴注入 - dig 篇
什麼是依賴注入?有時候一個結構體非常複雜,包含了非常多各種類型的屬性,這些屬性又包含了更多的屬性,當我們創建這樣一個結構體時需要編寫大量的代碼。面向接口編程可以讓我們的代碼避免耦合更具擴展性,但統一更換接口實現時需要大範圍的修改代碼。
依賴注入幫助我們解決類似的問題,依賴注入框架能夠自動解析依賴關係,幫助我們自動構建結構體實例。依賴注入可以對接口注入實例,讓整個代碼系統不用關注具體的接口實現。
由於 Go 語言靜態的特性,依賴注入在 Go 中應用並不廣泛,主要有兩種實現方式:代碼生成和反射。
上一篇已經介紹了 Go 依賴注入的中代碼生成實現的典型代表wire
,如果還沒閱讀可以點擊👉 在 Golang 中玩轉依賴注入 - wire 篇
本篇來介紹下反射實現的代表dig
安裝
因爲不需要命令行操作,因此只單獨引入 dig 庫即可
go get 'go.uber.org/dig@v1'
快速入門
dig 中也有一些概念需要理解
容器
依賴注入容器就是用來解析各種結構體依賴關係的對象,或者簡單點理解,他就是一個 dig 實例
c := dig.New()
Provide
和wire
中的provider
概念類似(沒看過wire
也沒關係),它就是一個普通的工廠函數,函數返回值是要被創建的類型,函數入參可以有多個,用來描述創建返回值需要依賴的其他類型。
type UserGateway struct {
conn *sql.DB
}
func (g UserGateway) GetUserName(id string) (name string, err error) {
row := g.conn.QueryRow("select name from users")
err = row.Scan(&name)
return
}
err := c.Provide(func(conn *sql.DB) (*UserGateway, error) {
return &UserGateway{conn}, nil
})
if err != nil {
// ...
}
上面代碼的意思是,創建UserGateway
依賴一個conn *sql.DB
那怎麼創建conn *sql.DB
呢?當然是提供一個conn *sql.DB
的provider
err = c.Provide(func(opt *Option) (*sql.DB, error) {
return sql.Open(opt.driver, opt.dsn)
})
if err != nil {
// ...
}
以此類推,我們還得提供一個opt *Option
的provider
err = c.Provide(func() *Option {
return &Option{
driver: "mysql",
dsn: "user:password@/dbname",
}
})
if err != nil {
// ...
}
於是構建UserGateway
條件就全部滿足了
Invoke
現在我們在依賴注入容器描述瞭如何創建每一個類型,以及創建每一個類型依賴的其他類型了。如果想要使用容器中的實例就變得非常簡單了
err := c.Invoke(func(g *UserGateway) {
name, err := g.GetUserName("liang")
if err != nil {
panic(err)
}
fmt.Println(name)
})
if err != nil {
panic(err)
}
當需要使用g *UserGateway
時,只需要像示例代碼一樣,放入Invoker
入參中,dig
會根據依賴關係正確創建g *UserGateway
實例並完成函數的執行
應用場景
官方文檔中提到了 dig 的推薦場景:
-
被集成到應用框架中
-
在服務啓動階段就完成全部依賴關係解析,不推薦在服務啓動後再使用
dig
下面示例是一個使用dig
依賴注入的 web 服務器,並且在啓動階段就完成了全部的依賴注入
完整代碼可以參考:https://github.com/liangwt/note_example/tree/main/dig
package main
import (
"github.com/gin-gonic/gin"
"github.com/liangwt/note/golang/demo/dig/internal"
)
func main() {
// provider代碼簡略
c := internal.Init()
r := gin.Default()
err := c.Invoke(func(g *internal.UserGateway) {
r.GET("/get_username", func(c *gin.Context) {
name, err := g.GetUserName(c.Query("id"))
if err != nil {
c.JSON(500, err.Error())
return
}
c.JSON(200, gin.H{
"name": name,
})
})
})
if err != nil {
panic(err)
}
r.Run()
}
進階用法
參數對象和返回值對象
前面提到provider
函數的入參和返回值可以是多個,但當參數或返回值太多時,可讀性較差
// internal.go
type PostGateway struct {
conn *sql.DB
}
type CommentGateway struct {
conn *sql.DB
}
type UserGateway struct {
conn *sql.DB
}
err = c.Provide(func(Logger *log.Logger, DB *sql.DB) (
Comments *CommentGateway,
Posts *PostGateway,
Users *UserGateway,
err error,
) {
return &CommentGateway{conn: DB},
&PostGateway{conn: DB},
&UserGateway{conn: DB},
nil
})
if err != nil {
// ...
}
// main.go
func main() {
c := internal.Init()
err := c.Invoke(func(g *internal.UserGateway) {
name, err := g.GetUserName("id")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(name)
})
if err != nil {
panic(err)
}
}
我們可以傳遞結構體來增強可讀性。和普通結構體參數 / 返回值不一樣,dig
要求的結構體必須內嵌dig.In
或者dig.Out
,下面的示例和上面的代碼是等價的
type Gateways struct {
dig.Out
Comments *CommentGateway
Posts *PostGateway
Users *UserGateway
}
type Connection struct {
dig.In
Logger *log.Logger
DB *sql.DB
}
err = c.Provide(func(conn Connection) (Gateways, error) {
return Gateways{
Comments: &CommentGateway{conn: conn.DB},
Posts: &PostGateway{conn: conn.DB},
Users: &UserGateway{conn: conn.DB},
}, nil
})
if err != nil {
// ...
}
在 Invoke 時也沒有區別,依舊可以直接使用UserGateway
而不是Gateways
// main.go
func main() {
c := internal.Init()
err := c.Invoke(func(g *internal.UserGateway) {
name, err := g.GetUserName("id")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(name)
})
if err != nil {
panic(err)
}
}
注意參數對象 / 返回值對象與普通結構體的區別
🌲 參數對象 / 返回值對象包含dig.In
、dig.Out
,雖然是一個結構體,但代表的是多個參數 / 返回值,結構體的每一個字段都代表着參數 / 返回值
對於上面的例子,Gateways
因爲包含dig.Out
,所以Invoke
時可以注入*CommentGateway
、*PostGateway
、*UserGateway
而下面例子Gateways
不包含dig.Out
只是普通的結構體,Invoke
時只能注入Gateways
,注入*UserGateway
將會報錯
type Gateways struct {
Comments *CommentGateway
Posts *PostGateway
Users *UserGateway
}
type Connection struct {
dig.In
Logger *log.Logger
DB *sql.DB
}
err = c.Provide(func(conn Connection) (Gateways, error) {
return Gateways{
Comments: &CommentGateway{conn: conn.DB},
Posts: &PostGateway{conn: conn.DB},
Users: &UserGateway{conn: conn.DB},
}, nil
})
if err != nil {
// ...
}
func main() {
c := internal.Init()
err := c.Invoke(func(g internal.Gateways) {
name, err := g.Users.GetUserName("id")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(name)
})
if err != nil {
panic(err)
}
}
🌲 參數對象 / 返回值對象不能是指針類型,普通類型是可以是指針類型的
可選依賴
有些依賴並不是必須的,因此可以通過結構體 tag:optional:"true"
把某些依賴標記成可選的。可選依賴只能在參數對象上使用
如下的示例中,即使沒有提供*redis.Client
的 provide 函數,Gateways
依舊可以被創建成功
type UserGateway struct {
conn *sql.DB
cache *redis.Client
}
func (g *UserGateway) GetUserName(id string) (name string, err error) {
if g.cache != nil {
name := g.cache.Get(id).Val()
if name != "" {
return name, nil
}
}
row := g.conn.QueryRow("select name from users")
err = row.Scan(&name)
if err != nil && g.cache != nil {
g.cache.Set(id, name, -1)
}
return
}
type Connection struct {
dig.In
Logger *log.Logger
Cache *redis.Client `optional:"true"`
DB *sql.DB
}
err = c.Provide(func(conn Connection) (Gateways, error) {
return Gateways{
Comments: &CommentGateway{conn: conn.DB},
Posts: &PostGateway{conn: conn.DB},
Users: &UserGateway{conn: conn.DB, cache: conn.Cache},
}, nil
})
if err != nil {
// ...
}
命名值
有時候我們可能同時依賴兩個相同類型的資源,例如讀寫分離的庫
type UserGateway struct {
roDB *sql.DB
rwDB *sql.DB
cache *redis.Client
}
func (g *UserGateway) GetUserName(id string) (name string, err error) {
if g.cache != nil {
name := g.cache.Get(id).Val()
if name != "" {
return name, nil
}
}
row := g.roDB.QueryRow("select name from users")
err = row.Scan(&name)
if err != nil && g.cache != nil {
g.cache.Set(id, name, -1)
}
return
}
以上場景無論我們怎麼提供兩個*sql.DB
的 provider,都無法區分出來哪一個是隻讀庫,哪一個是讀寫庫
err = c.Provide(func(opt *Option) (*sql.DB, *sql.DB, error) {
// 讀寫庫
rw, err := sql.Open(opt.driver, opt.dsn)
if err != nil {
return nil, nil, err
}
// 只讀庫
ro, err := sql.Open("mysql", "user:password@/ro_dbname")
if err != nil {
return nil, nil, err
}
return rw, ro, nil
})
if err != nil {
// ...
}
////////////////以下和上文代碼等價////////////////
// 讀寫庫
err = c.Provide(func(opt *Option) (*sql.DB, error) {
return sql.Open(opt.driver, opt.dsn)
})
if err != nil {
// ...
}
// 只讀庫
err = c.Provide(func() (*sql.DB, error) {
return sql.Open("mysql", "user:password@/ro_dbname")
})
if err != nil {
// ...
}
在構建*UserGateway
時,rwDB
, roDB
也無法對應以上兩個*sql.DB
err = c.Provide(func(Logger *log.Logger, rwDB, roDB *sql.DB) (
Comments *CommentGateway,
Posts *PostGateway,
Users *UserGateway,
err error,
) {
return &CommentGateway{rwDB: rwDB},
&PostGateway{rwDB: rwDB},
&UserGateway{rwDB: rwDB, roDB: roDB},
nil
})
if err != nil {
// ...
}
解決方案就是命名
首先我們要做的是分別給兩個*sql.DB
一個名字,用來區分兩個*sql.DB
。
如果各自使用獨立的函數,則可以使用dig.Name
// 讀寫庫
err = c.Provide(func(opt *Option) (*sql.DB, error) {
return sql.Open(opt.driver, opt.dsn)
}, dig.Name("rw"))
if err != nil {
// ...
}
// 只讀庫
err = c.Provide(func() (*sql.DB, error) {
return sql.Open("mysql", "user:password@/ro_dbname")
}, dig.Name("ro"))
if err != nil {
// ...
}
如果使用一個函數構建兩個*sql.DB
,則可以使用返回值對象的 tag
type DBResult struct {
dig.Out
RWDB *sql.DB `name:"rw"`
RODB *sql.DB `name:"ro"`
}
err = c.Provide(func(opt *Option) (DBResult, error) {
rw, err := sql.Open(opt.driver, opt.dsn)
if err != nil {
return DBResult{}, err
}
ro, err := sql.Open("mysql", "user:password@/ro_dbname")
if err != nil {
return DBResult{}, err
}
return DBResult{RWDB: rw, RODB: ro}, nil
})
if err != nil {
// ...
}
通過以上兩種方式,我們聲明瞭兩個*sql.DB
,哪一個是隻讀庫,哪一個是讀寫庫。下面要做的就是使用這兩個庫是指定對應關係。於是RODB *sql.DB
便會被注入只讀庫。
type Connection struct {
dig.In
Logger *log.Logger
Cache *redis.Client `optional:"true"`
RODB *sql.DB `name:"ro"`
RWDB *sql.DB `name:"rw"`
}
type Gateways struct {
dig.Out
Comments *CommentGateway
Posts *PostGateway
Users *UserGateway
}
err = c.Provide(func(conn Connection) (Gateways, error) {
return Gateways{
Comments: &CommentGateway{rwDB: conn.RWDB},
Posts: &PostGateway{rwDB: conn.RWDB},
Users: &UserGateway{rwDB: conn.RWDB, roDB: conn.RODB, cache: conn.Cache},
}, nil
})
if err != nil {
// ...
}
總結
本篇介紹了利用反射實現的依賴注入框架dig
。
可以看到dig
在使用被注入的依賴時,需要放入Invoke
的函數中,如果在代碼中任意使用,必然大幅影響代碼的可讀性。和wire
不同,如果缺少某項依賴dig
在編譯階段不會報錯,只有在運行時的 panic
基於以上兩點,我更推薦在服務啓動階段就利用dig
完成全部依賴關係解析,不推薦在服務啓動後再使用dig
dig
也被集成到了 https://github.com/uber-go/fx 的框架中,有機會我也會給大家介紹下 fx 框架
以上所有的示例的代碼都可以在 https://github.com/liangwt/note_example/tree/main/dig 找到
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/0DRrMNkQUm3bXt5d3wMAbg