Golang 代碼優化之依賴注入

本文介紹在 golang 中如何通過依賴注入 (Dependency Inject,簡稱 DI) 管理全局服務。

什麼是 DI

把有依賴關係的類放到容器中,解析出這些類的實例,就是依賴注入。

DI 的作用

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 https://github.com/uber-go/dig/issues/241

所以,我們在使用dig注入的時候,將如repo.NewPersonRepository這樣依賴ConfigDB的實例函數,在 main 函數里通過container.Provide注入進去,這樣僅調用一次,保證線程安全。

轉自:hongker

hongker.github.io/2021/03/31/golang-di/

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dOvp-GV3snMcMyzKw3QfaQ