本文介绍在golang中如何通过依赖注入(Dependency Inject,简称DI)管理全局服务。
什么是DI
把有依赖关系的类放到容器中,解析出这些类的实例,就是依赖注入。
DI的作用
- 反面例子
现在我们有一个http应用,先来看下常规开发的main.go:直接看1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70func main() {
// 生成config实例
config := NewConfig()
// 连接数据库
db, err := ConnectDatabase(config)
// 判断是否有错误
if err != nil {
panic(err)
}
// 生成repository实例,用于获取person数据,参数是db
personRepository := repo.NewPersonRepository(db)
// 生成service实例,用于调用repository的方法
personService := service.NewPersonService(config, personRepository)
// 生成http服务实例
server := NewServer(config, personService)
// 启动http服务
server.Run()
}
// Server
type Server struct {
config *config.Config
personService *service.PersonService
}
// Handler
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/people", s.people)
return mux
}
// Run
func (s *Server) Run() {
httpServer := &http.Server{
Addr: ":" + s.config.Port,
Handler: s.Handler(),
}
httpServer.ListenAndServe()
}
// people
func (s *Server) people(w http.ResponseWriter, r *http.Request) {
people := s.personService.FindAll()
bytes, _ := json.Marshal(people)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(bytes)
}
// NewServer
func NewServer(config *config.Config, service *service.PersonService) *Server {
return &Server{
config: config,
personService: service,
}
}
// 其他new方法
func NewConfig() *Config {
// ...
}
func ConnectDatabase(config *config.Config) (*sql.DB, error) {
// ...
}
func NewPersonRepository(database *sql.DB) *PersonRepository {
// ...
}
func NewPersonService(config *config.Config, repository *repo.PersonRepository) *PersonService {
// ...
}main()
,你会发现包含清晰的初始化流程。
但是仔细想想,随着业务的扩展,我们如果把所有实例都在main函数里生成,main函数将变得越来越臃肿。
而且这些基础服务的实例,如果在其他包里需要引入,你就得给每个需要用到服务的地方,通过参数的方式传递。类似service.NewPersonService(config, personRepository)
方法,将config
和personRepository
传递到service包。
- 问题
- 如何让main函数变得优雅?
- 如何管理全局服务实例?
- 如何减少重复实例化对象时传递如
config
这样的基础实例?
安装
我使用的是uber的dig包
1 | go get github.com/uber-go/dig |
优化main函数
1 | // 构建一个DI容器 |
这样的main函数不需要包含任何基础实例的初始化和参数传递的过程,可以称之:Perfect!
下面是对main函数里基础服务注入的流程说明:
- BuildContainer,只将各个基础服务的实例化方法注入到容器里,还没有调用这些方法来实例化基础服务
- container.Invoke,这里将会从容器里寻找server实例,来运行
server.Run()
。如果实例不存在,则调用其实例化的方法,也就是NewServer
- 因为
NewServer(config *config.Config, service *service.PersonService) *Server
依赖于config.Config
和service.PersonService
,故触发NewConfig
和NewPersonService
方法。 NewConfig
不依赖与任何实例,故可以成功返回config.Config
实例。NewPersonService(config *config.Config, repository *repo.PersonRepository) *PersonService
依赖config.Config
和repo.PersonRepository
,继而触发repo.NewPersonRepository
去实例化repo.PersonRepository
repo.NewPersonRepository
方法依赖于db
,故触发ConnectDatabase
方法,用来连接数据库,实例化db
实例- 最后递归倒推回去,完成所有实例的初始化与注入,调用
server.Run()
方法启动http服务。
注意,有依赖的初始化方法,需要放在前置依赖注入之后,比如container.Provide(ConnectDatabase)
就放在container.Provide(NewConfig)
之后。如果找不到初始化需要的依赖对象,在Invoke时就会报错。
踩坑
之前我通过下面的方式去获取容器里的基础实例:
1 | package app |
这样去获取基础实例是不正确的用法,因为dig
底层是通过一个map
来管理这些实例的,我们都知道map
不是线程安全的,在频繁调用时偶尔
会出现以下错误:
1 | concurrent map writes |
开发者回答如下:
1 | Hey, dig is not intended to be invoked from your system's hot path. We expect |
参考官方git里的一个issue,里面的代码能重现该异常: Invoke not concurrency safe
所以,我们在使用dig
注入的时候,将如repo.NewPersonRepository
这样依赖Config
和DB
的实例函数,在main函数里通过container.Provide
注入进去,这样仅调用一次,保证线程安全。
- 本文作者: Hongker
- 本文链接: https://hongker.github.io/2021/03/31/golang-di/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!