← 返回文章列表

控制反轉和依賴注入

理解控制反轉與依賴注入,以及它們在 Go 專案中的作用。

go

控制反轉(Inversion of Control, IoC)是物件導向程式設計中的一種設計原則,用來降低計算機程式碼之間的耦合程度,控制反轉最常見的方式就是依賴注入(Dependency Injection, DI),Wire 就是一個常見的依賴注入工具。

基本概念

控制反轉本質是一種把控制權交出去的設計思想:以前是你在程式碼裡自己 new 一個物件、自己決定它什麼時候建立、用誰當依賴、怎麼組裝呼叫鏈;IoC 之後,這些“建立 + 組裝 + 排程”的控制權交給外部(框架/容器/上層),你的業務程式碼只描述“我需要什麼、我提供什麼”,不負責把一切拼起來,能大大降低程式碼的耦合程度。

依賴注入是 IoC 最常見的方式之一,當 A 需要 B 的時候,不需要A 自己建立,直接透過外部建立好,透過引數傳遞進來,透過方法欄位進行注入,DI 將依賴關係表現得更加顯式。

Wire 是 Go 裡的編譯期依賴注入程式碼生成工具。它的用途是把你宣告的 provider(也就是建構函式)和 injector(也就是裝配入口函式)組合起來,自動生成一段等價於你手寫 new 然後逐層傳參組裝的初始化程式碼。你先定義一個 ProviderSet,比如 ProviderSet = wire.NewSet(NewData, NewRepo, NewUsecase, NewService 等),再在 wireApp 這種 injector 函式里呼叫 wire.Build 來表達你最終想得到的物件,比如我要一個 kratos.App。Wire 在生成階段會根據這些建構函式的型別簽名推導依賴關係,計算出正確的建立順序,檢查依賴是否缺失以及是否存在迴圈依賴,最後生成 wire_gen.go,在裡面給出真正可執行的 wireApp 實現,同時把需要釋放資源的 cleanup 函式也拼出來。執行時不需要容器,也不需要反射,本質上就是把裝配程式碼自動寫出來,讓初始化過程更清晰、更安全、更容易維護。

為什麼要降低耦合呢?

增加可維護性,防止牽一髮動全身

增加可測試性,測試位置更加精準,只跑邏輯本身,更加符合現在設計的理念

增加可替換性,降低程式碼迭代成本,可能只需要替換一個引數就能實現資料庫的替換,不需要進行大改

降低耦合也能提升團隊的協作效率,每個人只需要關心自己的一部分。

增加專案可靠性,這樣某個部分故障,不容易發生擴散,更容易找出問題。

控制反轉

控制反轉的常見方法

  • 構造器注入:把依賴作為建構函式引數傳入,依賴關係最顯式,Go 最推薦。

  • 方法/Setter 注入:透過 setter 或初始化方法注入,適合可選依賴或執行時替換。

  • 欄位/屬性注入:框架透過反射/標籤把依賴塞進結構體欄位,寫起來省事但依賴隱藏、可測試性差。

  • 介面 + 工廠(Factory):透過介面隔離具體實現,再用工廠負責建立實現物件,常用於“按配置選擇實現”。

  • 抽象工廠/構建器(Abstract Factory / Builder):當物件組裝很複雜、需要分步驟或多套組合方案時用。

  • 服務定位器(Service Locator):物件自己從“容器”裡按 key/型別取依賴(也是 IoC),但依賴不透明,一般不推薦做主方案。

  • 外掛化/登錄檔(Plugin / Registry):實現透過序號產生器制掛到 registry,執行時按名稱/能力發現與載入。

  • 函式式 Options(Functional Options):Go 常見,用 WithXxx() 注入可選依賴或配置,適合引數很多的構造。

  • Context/請求級注入(Request-scoped Injection):每個請求攜帶依賴(如 trace/span、使用者資訊、事務物件),常見於 web 框架中介軟體鏈。

  • 編譯期裝配(Codegen Wiring):依賴在編譯期被解析並生成裝配程式碼,執行時無容器/無反射(Wire、Dagger 這類)。

依賴注入

依賴注入常見工具

Go

  • 編譯期:Google Wire

  • 執行時容器:Uber dig、sarulabs/di、(較老) facebookgo/inject

  • 應用框架化生命週期:Uber fx(基於 dig,帶 OnStart/OnStop)

  • 其他思路:手寫 main 裝配、Functional Options(不算工具但很常用)

Java / JVM

  • Spring / Spring Boot(執行時容器,註解驅動)

  • Guice(執行時容器)

  • Dagger(編譯期為主,Android 常見)

  • Micronaut(偏編譯期/減少反射)

.NET

  • Microsoft.Extensions.DependencyInjection(官方內建 DI)

  • Autofac、Castle Windsor(第三方容器)

Node.js / TypeScript

  • NestJS(框架自帶 DI)

  • InversifyJS、tsyringe、typedi(容器/注入庫)

Python

  • dependency-injector(容器)

  • FastAPI Depends(更偏“依賴解析/函式注入”,request-scoped 很強)

  • Injector(Guice 風格)

Rust / C++(更少“容器化”,更多手動/編譯期)

  • Rust:更常見是手動裝配 + trait 抽象(DI 工具相對少)

  • C++:Boost.DI(編譯期)

Wire

Go社群中有很多依賴注入框架。比如:Uber的dig和Facebook的inject都使用反射來做執行時依賴注入。

wire是一個的 Google 開源的依賴注入工具,透過自動生成程式碼的方式在編譯期完成依賴注入。

wire 中有兩個核心概念:提供者(provider)和注入器(injector)。

Provider

provider 通常是一個 New 開頭的建構函式,表示實現了某一個結構體,也就是建立了一個物件

package demo

import (
    "context"
    "errors"
)

type A struct {
    Value int
}

func NewA(ctx context.Context, b B) (A, error) {
        if b.Value == 0 {
                return A{}, errors.New("cannot provide a when value is zero")
        }
        return A{Value: b.Value + 2}, nil
}

提供者函式可以分組為提供者函式集(provider set)。使用wire.NewSet 函式可以將多個提供者函式新增到一個集合中。如果經常同時使用多個提供者函式,這非常有用。

package demo

import (
    // ...
    "github.com/google/wire"
)

var ProviderSet = wire.NewSet(NewData, NewRepo, NewUsecase, NewService)

同時,還可以將其他提供者函式集新增到提供者函式集中。

Injector

應用程式中是用一個注入器來連線提供者,注入器就是一個按照依賴順序呼叫提供者。

要宣告一個注入器函式只需要在函式體中呼叫wire.Build。這個函式的返回值也無關緊要,只要它們的型別正確即可。這些值在生成的程式碼中將被忽略。

func wireApp(*conf.Server, *conf.Data, log.Helper) (*kratos.App, func(), error) {
    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

wire的安裝和使用

在 golang 中使用 install 安裝依賴包,透過在注射器同級目錄下使用 wire 指令即可完成自動依賴注入

go install github.com/google/wire/cmd/wire@latest

cd cmd/golist

wire

透過上面例子會生成一個 wire_gen.go檔案,此時依賴注入完成。

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
    "golist/internal/biz"
    "golist/internal/conf"
    "golist/internal/data"
    "golist/internal/server"
    "golist/internal/service"

    "github.com/go-kratos/kratos/v2"
    "github.com/go-kratos/kratos/v2/log"

    _ "go.uber.org/automaxprocs"
)

// Injectors from wire.go:

// wireApp init kratos application.
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {
    db, err := data.NewDB(confData)
    if err != nil {
        return nil, nil, err
    }
    dataData, cleanup, err := data.NewData(db, logger)
    if err != nil {
        return nil, nil, err
    }
    todoRepo := data.NewTodoRepo(dataData, logger)
    todoUsecase := biz.NewTodoUsecase(todoRepo, logger)
    todoService := service.NewTodoService(todoUsecase)
    grpcServer := server.NewGRPCServer(confServer, todoService, logger)
    httpServer := server.NewHTTPServer(confServer, todoService, logger)
    app := newApp(logger, grpcServer, httpServer)
    return app, func() {
        cleanup()
    }, nil
}

參考資料

依賴注入工具-wire

Wire 依賴注入

ChatGPT老師

關於這篇文章的日期

舊日期 2025-01-16 與文章發布記錄不符,現按舊站可確認的首次發布日 2026-02-13 整理。實際創作日期尚無法確認。

原始日期: 2025-01-16 21:15:06 +0800