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)方法,將configpersonRepository傳遞到 service 包。

問題

安裝

我使用的是 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 函數里基礎服務注入的流程說明:

注意,有依賴的初始化方法,需要放在前置依賴注入之後,比如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這樣依賴ConfigDB的實例函數,在 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