
控制反轉(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
}