基於泛型的輕量級依賴注入庫 do
在 Go 語言的開發實踐中,我們經常需要處理各種依賴關係,例如,一個 service
層可能依賴一個或多個 repository
層。如何優雅地管理這些依賴,是我們在項目開發中需要重點關注的問題。一個好的依賴管理方案,可以顯著提高代碼的可讀性、可維護性和可測試性。
今天,我們就來介紹一個 Go 語言生態中非常受歡迎的輕量級依賴注入庫:samber/do
。它基於 Go 1.18+ 的泛型特性,實現了一套類型安全的依賴注入工具集。相較於其他依賴注入框架(uber-go/dig、google/wire),samber/do
具有輕量、無代碼生成、無外部依賴等諸多優點,非常適合在中小型項目中使用。
本文將帶你全面瞭解 samber/do
的核心概念和使用方法,並通過一個簡單的 Web 應用示例,展示如何在實際項目中應用 samber/do
。
什麼是依賴注入?
在介紹 samber/do
之前,我們先來回顧一下什麼是依賴注入(Dependency Injection,簡稱 DI)。
依賴注入是一種軟件設計模式,它的核心思想是:一個對象不應該自己創建它所依賴的對象,而應該由外部的某個實體(我們稱之爲 “注入器” 或“容器”)來創建並 “注入” 給它。
這樣做的好處是顯而易見的:
-
解耦:對象與其依賴項之間的耦合度大大降低。我們可以在不修改對象本身代碼的情況下,替換其依賴的實現。
-
提高可測試性:在單元測試中,我們可以輕鬆地注入
mock
對象,從而實現對業務邏輯的獨立測試。 -
代碼更清晰:依賴關係的管理被集中到了一個地方,使得代碼的組織結構更加清晰。
在 Go 語言中,我們通常通過接口(interface
)來實現依賴注入。但是,當項目變得複雜,依賴關係網變得錯綜複雜時,手動管理依賴注入就會變得非常繁瑣。這時,一個好用的 DI 框架就能派上用場了。
samber/do
快速入門
samber/do
是一個純 Go 實現的極簡依賴注入庫,它的 API 非常簡單,幾乎沒有學習成本。它的核心思想是用工廠函數註冊依賴,按需獲取實例,自動處理依賴關係和生命週期。
下面我們通過一個簡單的例子來快速上手。
1. 安裝
首先,通過 go get
命令安裝 samber/do
:
go get github.com/samber/do
2. 核心概念
samber/do
的核心概念主要有三個:
-
Injector
(注入器):Injector
是do
庫的核心,它扮演着 DI 容器的角色,負責管理所有服務的生命週期。 -
Provider
(提供者):Provider
是一個函數,用於創建和返回一個服務的實例。我們通過do.Provide
或do.ProvideNamed
方法,將Provider
註冊到Injector
中。 -
Invoker
(調用者): 當我們需要使用一個服務時,通過do.Invoke
或do.MustInvoke
方法從Injector
中獲取該服務的實例。
3. 一個簡單的例子
下面,我們來看一個簡單的例子,演示如何使用 samber/do
來管理依賴。
假設我們有一個 UserService
,它依賴一個 UserRepository
來獲取用戶信息。
user_repository.go
package main
import"fmt"
type UserRepository struct{}
func NewUserRepository() *UserRepository {
return &UserRepository{}
}
func (r *UserRepository) GetUser(id int) string {
return fmt.Sprintf("User %d", id)
}
user_service.go
package main
import"github.com/samber/do"
type UserService struct {
repo *UserRepository
}
func NewUserService(i *do.Injector) (*UserService, error) {
repo := do.MustInvoke[*UserRepository](i)
return &UserService{repo: repo}, nil
}
func (s *UserService) GetUserName(id int) string {
return s.repo.GetUser(id)
}
main.go
package main
import (
"fmt"
"github.com/samber/do"
)
func main() {
// 1. 創建一個新的注入器
injector := do.New()
// 2. 註冊服務
// 註冊 UserRepository
do.Provide(injector, func(i *do.Injector) (*UserRepository, error) {
return NewUserRepository(), nil
})
// 註冊 UserService
do.Provide(injector, NewUserService)
// 3. 獲取並使用服務
userService := do.MustInvoke[*UserService](injector)
userName := userService.GetUserName(42)
fmt.Println(userName) // 輸出: User 42
// 4. 關閉注入器,釋放所有服務
injector.Shutdown()
}
在這個例子中,我們首先創建了一個 Injector
。然後,通過 do.Provide
方法,將 UserRepository
和 UserService
的構造函數註冊爲 Provider
。最後,通過 do.MustInvoke
獲取 UserService
的實例,並調用其方法。do
會自動處理 UserService
對 UserRepository
的依賴關係。
samber/do
的進階用法
除了基本的服務註冊和獲取,samber/do
還提供了許多實用的功能,以滿足更復雜的應用場景。
1. 命名服務依賴
有時,我們可能需要爲同一個類型註冊多個不同的實例。例如,我們可能需要連接兩個不同的數據庫。這時,就可以使用 “命名服務” 來區分它們。
package main
import (
"database/sql"
"fmt"
"log"
"github.com/samber/do"
_ "github.com/mattn/go-sqlite3"
)
func main() {
injector := do.New()
// 註冊名爲 "db1" 的數據庫連接
do.ProvideNamed(injector, "db1", func(i *do.Injector) (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./db1.sqlite")
if err != nil {
log.Fatal(err)
}
return db, nil
})
// 註冊名爲 "db2" 的數據庫連接
do.ProvideNamed(injector, "db2", func(i *do.Injector) (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./db2.sqlite")
if err != nil {
log.Fatal(err)
}
return db, nil
})
// 獲取名爲 "db1" 的數據庫連接
db1 := do.MustInvokeNamed[*sql.DB](injector, "db1")
fmt.Printf("DB1: %+v\n", db1)
// 獲取名爲 "db2" 的數據庫連接
db2 := do.MustInvokeNamed[*sql.DB](injector, "db2")
fmt.Printf("DB2: %+v\n", db2)
injector.Shutdown()
}
2. 服務的生命週期
samber/do
支持兩種服務生命週期:
-
Singleton(單例): 這是默認的模式。服務在第一次被
Invoke
時創建,之後每次Invoke
都會返回同一個實例。 -
Transient(瞬態): 服務在每次被
Invoke
時都會創建一個新的實例。
我們可以通過 do.ProvideTransient
來註冊一個瞬態服務。
package main
import (
"fmt"
"math/rand"
"github.com/samber/do"
)
type RandomService struct {
value int
}
func main() {
injector := do.New()
// 註冊一個瞬態的 RandomService
do.ProvideTransient(injector, func(i *do.Injector) (*RandomService, error) {
return &RandomService{value: rand.Int()}, nil
})
// 多次獲取 RandomService
s1 := do.MustInvoke[*RandomService](injector)
s2 := do.MustInvoke[*RandomService](injector)
fmt.Printf("s1.value: %d\n", s1.value)
fmt.Printf("s2.value: %d\n", s2.value)
fmt.Printf("s1 == s2: %v\n", s1 == s2) // 輸出: s1 == s2: false
}
3. 服務的健康檢查
在生產環境中,我們經常需要監控服務的健康狀態。samber/do
提供了優雅的服務健康檢查機制。我們只需要讓服務實現 do.Healthcheckable
接口即可。
package main
import (
"context"
"fmt"
"github.com/samber/do"
)
type DatabaseService struct{}
func (s *DatabaseService) HealthCheck() error {
// 在這裏實現數據庫連接的健康檢查邏輯
fmt.Println("Checking database connection...")
returnnil// 返回 nil 表示健康
}
func main() {
injector := do.New()
do.Provide(injector, func(i *do.Injector) (*DatabaseService, error) {
return &DatabaseService{}, nil
})
// 觸發所有實現了 Healthcheckable 接口的服務的健康檢查
health, err := injector.HealthCheck()
if err != nil {
fmt.Printf("Health check failed: %v\n", err)
} else {
fmt.Println("All services are healthy!")
}
for serviceName, serviceErr := range health {
if serviceErr != nil {
fmt.Printf("Service %s is unhealthy: %v\n", serviceName, serviceErr)
} else {
fmt.Printf("Service %s is healthy\n", serviceName)
}
}
}
4. 優雅關閉
當我們的應用需要關閉時,我們希望能夠優雅地釋放所有資源。samber/do
同樣提供了優雅關閉的支持。我們只需要讓服務實現 do.Shutdownable
接口。
package main
import (
"context"
"fmt"
"github.com/samber/do"
)
type WorkerService struct{}
func (s *WorkerService) Shutdown() error {
fmt.Println("Shutting down worker service...")
// 在這裏實現資源釋放的邏輯
returnnil
}
func main() {
injector := do.New()
do.Provide(injector, func(i *do.Injector) (*WorkerService, error) {
return &WorkerService{}, nil
})
// ... 應用運行 ...
// 關閉注入器,會以註冊的逆序調用所有實現了 Shutdownable 接口的服務的 Shutdown 方法
injector.Shutdown()
}
samber/do
還會自動監聽 SIGINT
和 SIGTERM
信號,在收到信號時自動調用 Shutdown
,非常方便。
總結
samber/do
是一個功能強大且易於使用的 Go 語言依賴注入庫。它基於 Go 1.18+ 的泛型特性,提供了類型安全的 API,並且沒有任何外部依賴和代碼生成,非常輕量。
通過本文的介紹,相信你已經對 samber/do
的核心概念和使用方法有了全面的瞭解。在你的下一個 Go 項目中,不妨試試用 samber/do
來管理依賴,相信它會給你帶來更愉快的開發體驗。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/lDeQn5OCFxa5sbTeGHEf2g