什么是依赖注入?有时候一个结构体非常复杂,包含了非常多各种类型的属性,这些属性又包含了更多的属性,当我们创建这样一个结构体时需要编写大量的代码。面向接口编程可以让我们的代码避免耦合更具扩展性,但统一更换接口实现时需要大范围的修改代码。
依赖注入帮助我们解决类似的问题,依赖注入框架能够自动解析依赖关系,帮助我们自动构建结构体实例。依赖注入可以对接口注入实例,让整个代码系统不用关注具体的接口实现。
由于Go语言静态的特性,依赖注入在Go中应用并不广泛,主要有两种实现方式:代码生成和反射。
以前介绍过Go依赖注入的中代码生成实现的典型代表wire
,如果还没阅读可以点击👉 golang中的依赖注入之wire
本篇来介绍下反射实现的代表dig
安装
因为不需要命令行操作,因此只单独引入dig库即可
arduino
go get 'go.uber.org/dig@v1'
快速入门
dig中也有一些概念需要理解
容器
依赖注入容器就是用来解析各种结构体依赖关系的对象,或者简单点理解,他就是一个dig实例
css
c := dig.New()
Provide
和wire
中的provider
概念类似(没看过wire
也没关系),它就是一个普通的工厂函数,函数返回值是要被创建的类型,函数入参可以有多个,用来描述创建返回值需要依赖的其他类型。
go
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
go
err = c.Provide(func(opt *Option) (*sql.DB, error) {
return sql.Open(opt.driver, opt.dsn)
})
if err != nil {
// ...
}
以此类推,我们还得提供一个opt *Option
的provider
go
err = c.Provide(func() *Option {
return &Option{
driver: "mysql",
dsn: "user:password@/dbname",
}
})
if err != nil {
// ...
}
于是构建UserGateway
条件就全部满足了
Invoke
现在我们在依赖注入容器描述了如何创建每一个类型,以及创建每一个类型依赖的其他类型了。如果想要使用容器中的实例就变得非常简单了
go
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服务器,并且在启动阶段就完成了全部的依赖注入
完整代码可以参考:github.com/liangwt/not...
go
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
函数的入参和返回值可以是多个,但当参数或返回值太多时,可读性较差
go
// 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 {
// ...
}
go
// 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
,下面的示例和上面的代码是等价的
go
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
go
// 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
将会报错
go
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
依旧可以被创建成功
go
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 {
// ...
}
命名值
有时候我们可能同时依赖两个相同类型的资源,例如读写分离的库
go
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,都无法区分出来哪一个是只读库,哪一个是读写库
go
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
go
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
go
// 读写库
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
go
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
便会被注入只读库。
go
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
也被集成到了github.com/uber-go/fx的框架中,有机会我也会给大家介绍下fx框架
以上所有的示例的代码都可以在github.com/liangwt/not...找到
✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的后端知识 ✨