golang-dependency-injection

golang-dependency-injection

熱門

Golang 依賴注入(DI)的完整指南。涵蓋為何 DI 重要(可測試性、鬆散耦合、關注點分離、生命週期管理)、手動建構子注入,以及 DI 函式庫比較(google/wire、uber-go/dig、uber-go/fx、samber/do)。在設計服務架構、設定依賴注入、重構緊耦合程式碼、管理單例或服務工廠,或使用者詢問控制反轉、服務容器、Go 中的依賴連接時,使用此技能。如需特定 DI 函式庫,請參閱 `samber/cc-skills-golang@golang-google-wire`、`samber/cc-skills-golang@golang-uber-dig`、`samber/cc-skills-golang@golang-uber-fx` 或 `samber/cc-skills-golang@golang-samber-do` 技能。

2261星標
150分支
更新於 2026/6/6
SKILL.md
readonlyread-only
name
golang-dependency-injection
description

Golang 依賴注入(DI)的完整指南。涵蓋為何 DI 重要(可測試性、鬆散耦合、關注點分離、生命週期管理)、手動建構子注入,以及 DI 函式庫比較(google/wire、uber-go/dig、uber-go/fx、samber/do)。在設計服務架構、設定依賴注入、重構緊耦合程式碼、管理單例或服務工廠,或使用者詢問控制反轉、服務容器、Go 中的依賴連接時,使用此技能。如需特定 DI 函式庫,請參閱 `samber/cc-skills-golang@golang-google-wire`、`samber/cc-skills-golang@golang-uber-dig`、`samber/cc-skills-golang@golang-uber-fx` 或 `samber/cc-skills-golang@golang-samber-do` 技能。

角色: 你是一位 Go 軟體架構師。你引導團隊走向可測試、鬆散耦合的設計——你選擇最簡單的 DI 方法來解決問題,絕不過度設計。

模式:

  • 設計模式(新專案、新服務,或為現有 DI 設定新增服務):評估現有依賴圖與生命週期需求;從決策表中推薦手動注入或某個函式庫;然後產生連接程式碼。
  • 重構模式(現有緊耦合程式碼):使用最多 3 個平行子代理——代理 1 識別全域變數與 init() 服務設定,代理 2 映射應改為介面的具體型別依賴,代理 3 找出服務定位器反模式(容器作為參數傳遞)——然後彙整結果並提出遷移計畫。

社群預設值。 明確取代 samber/cc-skills-golang@golang-dependency-injection 技能的團隊技能具有優先權。

Go 中的依賴注入

依賴注入(DI)是將依賴傳遞給元件,而非讓元件自行建立或尋找它們。在 Go 中,這是建構可測試、鬆散耦合應用程式的方式——你的服務宣告它們需要的東西,而呼叫者(或容器)提供之。

此技能並非詳盡無遺。使用 DI 函式庫(google/wire、uber-go/dig、uber-go/fx、samber/do)時,請參閱該函式庫的官方文件與程式碼範例以取得最新的 API 簽章。

關於基於介面的設計基礎(接受介面,回傳結構),請參閱 samber/cc-skills-golang@golang-structs-interfaces 技能。

最佳實務摘要

  1. 依賴必須透過建構子注入——絕不使用全域變數或 init() 進行服務設定
  2. 小型專案(< 10 個服務)應使用手動建構子注入——不需要函式庫
  3. 介面必須在消費處定義,而非實作處——接受介面,回傳結構
  4. 絕不使用全域註冊表或套件層級的服務定位器
  5. DI 容器只能存在於組合根(main() 或應用程式啟動處)——絕不將容器作為依賴傳遞
  6. 偏好懶初始化——僅在首次請求時建立服務
  7. 對有狀態服務使用單例(資料庫連線、快取),對無狀態服務使用暫時性
  8. 在介面邊界進行模擬——DI 讓這變得簡單
  9. 保持依賴圖淺層——深層鏈表示設計問題
  10. 根據專案規模與團隊選擇合適的 DI 函式庫——請參閱下方的決策表

為何需要依賴注入?

沒有 DI 的問題 DI 如何解決
函式自行建立依賴 依賴被注入——可自由切換實作
測試需要真實資料庫、API 在測試中傳遞模擬實作
變更一個元件會破壞其他元件 透過介面鬆散耦合——元件不知道彼此內部細節
服務在各處初始化 集中式容器管理生命週期(單例、工廠、懶載入)
所有服務在啟動時載入 懶載入——僅在首次請求時建立服務
全域狀態與 init() 函式 啟動時明確連接——可預測、可除錯

DI 在具有許多相互連接服務的應用程式中表現出色——HTTP 伺服器、微服務、帶有外掛的 CLI 工具。對於只有 2-3 個函式的小型腳本,手動連接即可。不要過度設計。

手動建構子注入(無函式庫)

對於小型專案,透過建構子傳遞依賴。請參閱手動 DI 範例以取得完整的應用程式範例。

// ✓ 好——明確的依賴,可測試
type UserService struct {
    db     UserStore
    mailer Mailer
    logger *slog.Logger
}

func NewUserService(db UserStore, mailer Mailer, logger *slog.Logger) *UserService {
    return &UserService{db: db, mailer: mailer, logger: logger}
}

// main.go — 手動連接
func main() {
    logger := slog.Default()
    db := postgres.NewUserStore(connStr)
    mailer := smtp.NewMailer(smtpAddr)
    userSvc := NewUserService(db, mailer, logger)
    orderSvc := NewOrderService(db, logger)
    api := NewAPI(userSvc, orderSvc, logger)
    api.ListenAndServe(":8080")
}
// ✗ 不好——硬編碼的依賴,不可測試
type UserService struct {
    db *sql.DB
}

func NewUserService() *UserService {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // 隱藏依賴
    return &UserService{db: db}
}

手動 DI 在以下情況會失效:

  • 你有 15 個以上具有交叉依賴的服務
  • 你需要生命週期管理(健康檢查、優雅關機)
  • 你想要懶初始化或作用域容器
  • 連接順序變得脆弱且難以維護

DI 函式庫比較

Go 有三種主要的 DI 函式庫方法:

決策表

標準 手動 google/wire uber-go/dig + fx samber/do
專案規模 小型(< 10 個服務) 中大型 大型 任何規模
型別安全 編譯時期 編譯時期(程式碼產生) 執行時期(反射) 編譯時期(泛型)
程式碼產生 需要(wire_gen.go
反射
API 風格 不適用 提供者集合 + 建置標籤 結構標籤 + 裝飾器 簡單的泛型函式
懶載入 手動 不適用(全部急切) 內建(fx) 內建
單例 手動 內建 內建 內建
暫時性/工廠 手動 手動 內建 內建
作用域/模組 手動 提供者集合 模組系統(fx) 內建(階層式)
健康檢查 手動 手動 手動 內建介面
優雅關機 手動 手動 內建(fx) 內建介面
容器複製 不適用 不適用 不適用 內建
除錯 列印陳述式 編譯錯誤 fx.Visualize() ExplainInjector()、網頁介面
Go 版本 任何 任何 任何 1.18+(泛型)
學習曲線 中等

快速比較:同一個應用程式,四種方式

依賴圖:Config -> Database -> UserStore -> UserService -> API

手動:

cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// 沒有自動關機、健康檢查或懶載入

google/wire:

// wire.go — 然後執行:wire ./...
func InitializeAPI() (*API, error) {
    wire.Build(NewConfig, NewDatabase, NewUserStore, NewUserService, NewAPI)
    return nil, nil
}
// 沒有生命週期鉤子(OnStart/OnStop)或健康檢查;透過提供者回傳的 func() 進行清理

uber-go/fx:

app := fx.New(
    fx.Provide(NewConfig, NewDatabase, NewUserStore, NewUserService),
    fx.Invoke(func(api *API) { api.Run() }),
)
app.Run() // 管理生命週期,但基於反射

samber/do:

i := do.New()
do.Provide(i, NewConfig)
do.Provide(i, NewDatabase)    // 自動關機 + 健康檢查
do.Provide(i, NewUserStore)
do.Provide(i, NewUserService)
api := do.MustInvoke[*API](i)
api.Run()
// defer i.Shutdown() — 自動處理所有清理

使用 DI 進行測試

DI 讓測試變得簡單——注入模擬實作而非真實實作:

// 定義一個模擬
type MockUserStore struct {
    users map[string]*User
}

func (m *MockUserStore) FindByID(ctx context.Context, id string) (*User, error) {
    u, ok := m.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return u, nil
}

// 使用手動注入進行測試
func TestUserService_GetUser(t *testing.T) {
    mock := &MockUserStore{
        users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
    }
    svc := NewUserService(mock, nil, slog.Default())

    user, err := svc.GetUser(context.Background(), "1")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("got %q, want %q", user.Name, "Alice")
    }
}

使用 samber/do 進行測試——複製與覆蓋

容器複製會建立一個隔離的副本,你可以在其中僅覆蓋需要模擬的服務:

func TestUserService_WithDo(t *testing.T) {
    // 建立一個帶有模擬實作的測試注入器
    testInjector := do.New()

    // 提供模擬的 UserStore 介面
    do.OverrideValue[UserStore](testInjector, &MockUserStore{
        users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
    })

    // 根據需要提供其他真實服務
    do.Provide[*slog.Logger](testInjector, func(i *do.Injector) (*slog.Logger, error) {
        return slog.Default(), nil
    })

    svc := do.MustInvoke[*UserService](testInjector)
    user, err := svc.GetUser(context.Background(), "1")
    // ... 斷言
}

這對於整合測試特別有用,你希望大多數服務是真實的,但需要模擬特定邊界(資料庫、外部 API、郵件發送器)。

何時採用 DI 函式庫

訊號 行動
< 10 個服務,簡單依賴 維持手動建構子注入
10-20 個服務,有一些橫切關注點 考慮使用 DI 函式庫
20+ 個服務,需要生命週期管理 強烈建議使用
需要健康檢查、優雅關機 使用具有內建生命週期支援的函式庫
團隊不熟悉 DI 概念 從手動開始,逐步遷移

常見錯誤

錯誤 修正
全域變數作為依賴 透過建構子或 DI 容器傳遞
使用 init() 進行服務設定 main() 或容器中明確初始化
依賴具體型別 在消費邊界接受介面
到處傳遞容器(服務定位器) 注入特定依賴,而非容器
深層依賴鏈(A->B->C->D->E) 扁平化——大多數服務應直接依賴儲存庫和設定
每個請求建立新容器 每個應用程式一個容器;使用作用域進行請求層級隔離

交叉參考

  • → 參閱 samber/cc-skills-golang@golang-samber-do 技能以取得詳細的 samber/do 使用模式
  • → 參閱 samber/cc-skills-golang@golang-structs-interfaces 技能以取得介面設計與組合
  • → 參閱 samber/cc-skills-golang@golang-testing 技能以取得使用依賴注入進行測試
  • → 參閱 samber/cc-skills-golang@golang-project-layout 技能以取得 DI 初始化位置

參考資料