文章來源: Dependency Injection in Go
關於做者: Drew Olson
做者博客: software is fun
譯者按:本文用於介紹DI和golang中DI庫dig的簡單使用,適合對go有必定了解的開發者。
我最近使用Go建立了一個小項目,因爲最近幾年一直用Java,我馬上就被Go語言生態裏依賴注入(DI)的缺失震驚了。我決定嘗試用Uber的dig庫來建立個人項目,結果很是不錯。git
我發覺DI幫我解決了不少在之前的Go應用中遇到的問題——init
函數的過分使用,全局變量的濫用和複雜的應用初始化設置。
在這篇文章中,我會介紹DI,並展現一個應用在使用DI先後的區別(使用dig庫)。github
依賴注入是這樣一個概念,你的組件(在go中一般是structs
)應該在被建立時接收到它們的依賴。這與組件初始化過程當中建立本身的依賴的反面模式恰好相反。讓咱們來看一個例子。golang
假設你有一個Server
結構,要實現其功能,須要一個Config
結構。一種辦法是在Server
初始化時,構建它本身的Config
。sql
type Server struct { config *Config } func New() *Server { return &Server{ config: buildMyConfigSomehow(), } }
這樣看上去很方便,調用者甚至不須要知道咱們的Server
須要訪問Config
。這些對函數的使用者來講都是隱藏的。數據庫
然而這樣也有些缺點。首先,若是咱們想改變Config
建立的方式,咱們必須修改全部調用了它的建立方法的代碼。假設,舉個例子,咱們的buildMyConfigSomehow
函數如今須要一個參數,每一個調用將必需訪問那個參數,且須要將它傳遞給buildMyConfigSomehow
方法。json
並且,mock Config
會變得很棘手。咱們必須經過某種方式去觸及到New
函數的內部,去鼓搗Config
的建立。服務器
下面是使用DI的方式:app
type Server struct { config *Config } func New(config *Config) *Server { return &Server{ config: config, } }
如今Server
的建立與Config
的建立解除了耦合,咱們能夠隨意選擇建立Config
的邏輯,而後將結果數據傳遞給New
方法。框架
並且,若是Config
是個接口,mock就會變得更容易,只要實現了接口,均可傳遞給New
方法。這使咱們對Server
進行mock變得容易。ide
主要的負面影響是,咱們在建立Server
前必須手動建立Config
,這一點比較痛苦。咱們建立了一個「依賴圖」——咱們必須首先建立Config
由於Server
依賴它。在現實的應用中,依賴圖可能變得很是大,這會致使建立全部你的應用須要的組件的邏輯很是複雜。
這正是DI框架能提供幫助的地方。一個DI框架通常有兩個主要功用:
一個DI框架通常基於你告訴框架的「提供者」來建立一個圖(graph),而且決定如何建立你的object。抽象的概念比較難以理解,因此讓咱們看一個合適的例子。
咱們將要看一段代碼,一個在接收到客戶端GET
people
請求時,會發送一個JSON響應的HTTP服務器。簡單起見,咱們吧代碼都放在main
包裏,不過現實中千萬別這麼幹。所有的實例代碼能夠在此處下載。
首先,讓咱們看一下Person
結構,除了一些JSON tag以外,它沒有任何行爲定義。
type Person struct { Id int `json:"id"` Name string `json:"name"` Age int `json:"age"` }
一個Person
有Id
,Name
和Age
,沒別的了。
接下來,咱們看一下Config
,相似於Person
,它沒有任何依賴,不一樣於Person
的是,咱們將提供一個構造器(constructor)。
type Config struct { Enabled bool DatabasePath string Port string } func NewConfig() *Config { return &Config{ Enabled: true, DatabasePath: "./example.db", Port: "8000", } }
Enabled
控制應用是否應該返回真實數據。DatabasePath
表示數據庫位置(咱們使用sqlite)。Port
表示服務器運行時監聽的端口。
這個函數用於打開數據庫鏈接,它依賴於Config
並返回一個*sql.DB
。
func ConnectDatabase(config *Config) (*sql.DB, error) { return sql.Open("sqlite3", config.DatabasePath) }
接下來咱們會看到PersonRepository
。這個結構負責從數據庫中獲取people數據並將其反序列化,保存到Person
結構當中。
type PersonRepository struct { database *sql.DB } func (repository *PersonRepository) FindAll() []*Person { rows, _ := repository.database.Query( `SELECT id, name, age FROM people;` ) defer rows.Close() people := []*Person{} for rows.Next() { var ( id int name string age int ) rows.Scan(&id, &name, &age) people = append(people, &Person{ Id: id, Name: name, Age: age, }) } return people } func NewPersonRepository(database *sql.DB) *PersonRepository { return &PersonRepository{database: database} }
PersonRepository
須要一個待建立的數據庫鏈接。它暴露一個叫作FindAll
的函數,這個函數使用數據庫鏈接,並返回一個包含數據庫數據的Person
順序表。
爲了在HTTP Server和PersonRepository
中間新增一層,咱們將會建立一個PersonService
。
type PersonService struct { config *Config repository *PersonRepository } func (service *PersonService) FindAll() []*Person { if service.config.Enabled { return service.repository.FindAll() } return []*Person{} } func NewPersonService(config *Config, repository *PersonRepository) *PersonService { return &PersonService{config: config, repository: repository} }
咱們的PersonService
依賴於Config
和PersonRepository
。它暴露一個叫作FindAll
的函數,根據應用是不是enabled去調用PersonRepository
。
最後,咱們來到Server
,負責運行一個HTTP Server,而且把請求分配到PersonService
處理。
type Server struct { config *Config personService *PersonService } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/people", s.people) return mux } func (s *Server) Run() { httpServer := &http.Server{ Addr: ":" + s.config.Port, Handler: s.Handler(), } httpServer.ListenAndServe() } 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) } func NewServer(config *Config, service *PersonService) *Server { return &Server{ config: config, personService: service, } }
Server
依賴於PersonService
和Config
。
好了,咱們如今知道咱們系統的全部組件了。如今怎麼他媽的初始化它們而且啓動咱們的系統?
func main() { config := NewConfig() db, err := ConnectDatabase(config) if err != nil { panic(err) } personRepository := NewPersonRepository(db) personService := NewPersonService(config, personRepository) server := NewServer(config, personService) server.Run() }
咱們首先建立Config
,而後使用Config
建立數據庫鏈接。接下來咱們能夠建立PersonRepository
,這樣就能夠建立PersonService
了,最後,咱們使用這些來建立Server
並啓動它。
呼,剛纔那操做太複雜了。更糟糕的是,當咱們的應用變得更復雜時,咱們的main
的複雜度會持續增加。每次咱們的任何一個組件新增一個依賴,爲了建立這個組件,咱們都不得不琢磨這個依賴在main
中的順序和邏輯。
因此你也許會猜,一個依賴注入框架能夠幫咱們解決這個問題,咱們來試一下怎麼用。
「容器(container)」這個屬於在DI框架中,一般表示你存放「提供者(provider)」和獲取建立好的對象的一個地方。dig
庫提供給咱們Provide
函數,用於添加咱們本身的提供者;還有Invoke
函數,用於從容器中獲取建立好的對象。
首先,咱們建立一個新的容器。
container := dig.New()
如今咱們能夠添加新的提供者,方法很簡單,咱們用container調用Provide
函數,這個函數接收一個參數:一個函數。這個參數函數能夠有任意數量的參數(表明將要建立的組件的依賴)以及一個或兩個返回值(表明這個函數提供的組件,以及一個可選的error)。
container.Provide(func() *Config { return NewConfig() })
上述代碼表示,「我提供一個Config
給容器,爲了建立它,我並不須要其餘東西。」。如今咱們已經告訴了容器如何建立一個Config
類型,咱們可使用它來建立其餘類型了。
container.Provide(func(config *Config) (*sql.DB, error) { return ConnectDatabase(config) })
這部分代碼表示,「我提供一個*sql.DB
類型給容器,爲了建立它,我須要一個Config
,我可能會返回一個可選的error」。
在這兩個例子中,咱們有點囉嗦了,由於咱們已經定義了NewConfig
和ConnectDatabase
函數,咱們能夠直接將它們做爲提供者給容器使用。
container.Provide(NewConfig) container.Provide(ConnectDatabase)
如今,咱們能夠直接向容器請求一個構造好的組件,任何咱們給出了提供者的類型均可以。咱們用Invoke
函數來作這個操做。Invoke
函數接收一個參數——一個接收任意數量參數的函數,這些參數正是咱們想要容器爲咱們建立的組件。
container.Invoke(func(database *sql.DB) { // sql.DB is ready to use here })
容器的操做很聰明,它是這樣作的:
*sql.DB
ConnectDatabase
提供了這種類型ConnectDatabase
依賴一個Config
Config
的提供者,NewConfig
函數NewConfig
函數沒有任何依賴,因而被調用NewConfig
的返回值是一個Config
,被做爲參數傳遞給ConnectDatabase
ConnectionData
的結果是一個*sql.DB
,被傳回給Invoke
的調用者容器爲咱們作了不少事,實際上它作的更多。容器只會爲每種類型提供一個實例,這意味着咱們永遠不會意外的建立了第二個數據庫鏈接,即便咱們在多個地方調用(好比多個repository)。
如今咱們知道dig
容器是如何工做的了,讓咱們用它來建立一個更好的main。
func BuildContainer() *dig.Container { container := dig.New() container.Provide(NewConfig) container.Provide(ConnectDatabase) container.Provide(NewPersonRepository) container.Provide(NewPersonService) container.Provide(NewServer) return container } func main() { container := BuildContainer() err := container.Invoke(func(server *Server) { server.Run() }) if err != nil { panic(err) } }
除了Invoke
的error
返回,其它的咱們都見過了。若是任何被Invoke
使用的提供者返回錯誤,對Invoke
的調用就會中止,並返回error。
即便這個例子很小,應該也足以看出這種方法相對於「標準」main的一些好處了。隨着咱們的應用規模的增加,好處也會更多更明顯。
其中一個最重要的好處就是解耦組件的建立和依賴的建立。比方說,咱們的PersonRepository
須要訪問Config
,咱們只須要修改構造器NewPersonRepository
,給它加上一個Config
參數,其它什麼都不用變。
也有一些其它的好處,減小了全局的狀態,減小了init
的調用(依賴會在被須要時延遲建立,且只會建立一次,避免了有報錯傾向的init
調用),讓獨立組件的測試更加容易。想象下建立一個容器,去請求一個建立好的對象作測試,或者mock全部的依賴並建立對象。全部的這些都會隨着DI的引入變得容易。
我相信依賴注入幫助咱們建立更健壯,更易測試的程序。隨着應用程序規模的增加,這一點也更加顛撲不破。Go很是適合建立大型的應用程序,而且dig
是一個很好的DI工具。我認爲Go社區應該接受DI而且在儘量多的應用程序中使用它。
Google最近發佈了它們本身的DI容器,叫作wire。它經過代碼生成的方式避免了運行時反射。比起dig我更建議用wire。