Go 中 main 函數的初始化優化 - 依賴注入
原文鏈接:https://hongker.github.io/2021/03/31/golang-di/
本文介紹在 golang 中如何通過依賴注入 (Dependency Inject,簡稱 DI) 管理全局服務。
什麼是 DI
把有依賴關係的類放到容器中,解析出這些類的實例,就是依賴注入。
DI 的作用
反面例子
現在我們有一個 http 應用,先來看下常規開發的 main.go:
func 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 包
go get github.com/uber-go/dig
優化 main 函數
// 構建一個DI容器
func BuildContainer() *dig.Container {
container := dig.New()
// 注入config的實例化方法
container.Provide(NewConfig)
// 注入database的實例化方法
container.Provide(ConnectDatabase)
// 注入repository的實例化方法
container.Provide(repo.NewPersonRepository)
// 注入service的實例化方法
container.Provide(service.NewPersonService)
// 注入server
container.Provide(NewServer)
return container
}
func main() {
container := BuildContainer()
err := container.Invoke(func(server *Server) {
server.Run()
})
if err != nil {
panic(err)
}
}
這樣的 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 時就會報錯。
踩坑
之前我通過下面的方式去獲取容器裏的基礎實例:
package app
// Config 配置文件
func Config() (conf *config.Config) {
_ = container.Invoke(func(c *config.Config) {
conf = c
})
return
}
// 其他package
fmt.Println(app.Config().GetString("someKey"))
這樣去獲取基礎實例是不正確的用法,因爲dig
底層是通過一個map
來管理這些實例的,我們都知道map
不是線程安全的,在頻繁調用時偶爾
會出現以下錯誤:
concurrent map writes
開發者回答如下:
Hey, dig is not intended to be invoked from your system's hot path. We expect
it to be invoked at most once during startup, and definitely not concurrently.
To discourage usage on the hot path, we have kept the APIs thread-unsafe.
參考官方 git 裏的一個 issue, 裏面的代碼能重現該異常: Invoke not concurrency safe[1]
所以,我們在使用dig
注入的時候,將如repo.NewPersonRepository
這樣依賴Config
和DB
的實例函數,在 main 函數里通過container.Provide
注入進去,這樣僅調用一次,保證線程安全。
參考資料
[1]
Invoke not concurrency safe: https://github.com/uber-go/dig/issues/241
來自 Go 生態
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6xMX1VLKGMytHE6OMeAuYQ