基於泛型的輕量級依賴注入庫 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)。

依賴注入是一種軟件設計模式,它的核心思想是:一個對象不應該自己創建它所依賴的對象,而應該由外部的某個實體(我們稱之爲 “注入器” 或“容器”)來創建並 “注入” 給它。

這樣做的好處是顯而易見的:

在 Go 語言中,我們通常通過接口(interface)來實現依賴注入。但是,當項目變得複雜,依賴關係網變得錯綜複雜時,手動管理依賴注入就會變得非常繁瑣。這時,一個好用的 DI 框架就能派上用場了。

samber/do 快速入門

samber/do 是一個純 Go 實現的極簡依賴注入庫,它的 API 非常簡單,幾乎沒有學習成本。它的核心思想是用工廠函數註冊依賴,按需獲取實例,自動處理依賴關係和生命週期。

下面我們通過一個簡單的例子來快速上手。

1. 安裝

首先,通過 go get 命令安裝 samber/do

go get github.com/samber/do

2. 核心概念

samber/do 的核心概念主要有三個:

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 支持兩種服務生命週期:

我們可以通過 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