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





