文章

依赖注入

依赖注入

为什么需要依赖注入

// ❌ 硬编码依赖:紧耦合,难以测试
type UserService struct {
    db *sql.DB  // 直接依赖具体实现
}
func NewUserService() *UserService {
    db, _ := sql.Open("mysql", "...")  // 内部创建依赖
    return &UserService{db: db}
}

// ✓ 依赖注入:依赖通过参数传入
type UserService struct {
    repo UserRepository  // 依赖接口
}
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}
// 测试时可以传入 MockUserRepository

手写依赖注入(推荐简单项目)

// internal/app/app.go
package app

type App struct {
    server *http.Server
    db     *sql.DB
    redis  *redis.Client
}

func New(cfg *config.Config, logger *slog.Logger) (*App, error) {
    // 1. 基础设施
    db, err := initDB(cfg.Database)
    if err != nil {
        return nil, fmt.Errorf("init db: %w", err)
    }

    redis, err := initRedis(cfg.Redis)
    if err != nil {
        return nil, fmt.Errorf("init redis: %w", err)
    }

    // 2. Repository 层
    userRepo := persistence.NewUserRepository(db)
    orderRepo := persistence.NewOrderRepository(db)

    // 3. Service 层
    userSvc := service.NewUserService(userRepo, redis, logger)
    orderSvc := service.NewOrderService(orderRepo, userSvc, logger)

    // 4. Handler 层
    userHandler := http.NewUserHandler(userSvc)
    orderHandler := http.NewOrderHandler(orderSvc)

    // 5. 路由
    mux := chi.NewRouter()
    userHandler.Register(mux)
    orderHandler.Register(mux)

    // 6. 服务器
    server := &http.Server{
        Addr:         fmt.Sprintf(":%d", cfg.Server.Port),
        Handler:      middleware.Chain(mux, middleware.Recovery, middleware.Logging),
        ReadTimeout:  cfg.Server.ReadTimeout,
        WriteTimeout: cfg.Server.WriteTimeout,
    }

    return &App{
        server: server,
        db:     db,
        redis:  redis,
    }, nil
}

func (a *App) Run() error {
    return a.server.ListenAndServe()
}

func (a *App) Shutdown(ctx context.Context) error {
    a.server.Shutdown(ctx)
    a.db.Close()
    a.redis.Close()
    return nil
}

手写 DI 的优缺点

优点缺点
零依赖依赖多时 init 函数很大
编译时检查添加新依赖需修改 init
完全可控循环依赖需手动解决
代码直观-

Wire(Google 代码生成 DI)

// 安装:go install github.com/google/wire/cmd/wire@latest

// wire.go(代码生成文件,会被 wire 工具处理)
//go:build wireinject
// +build wireinject

package app

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

// Provider 函数:创建依赖
func ProvideDB(cfg *config.DatabaseConfig) (*sql.DB, error) {
    return sql.Open(cfg.Driver, cfg.DSN())
}

func ProvideRedis(cfg *config.RedisConfig) (*redis.Client, error) {
    return redis.NewClient(...), nil
}

func ProvideUserRepo(db *sql.DB) *persistence.UserRepository {
    return persistence.NewUserRepository(db)
}

func ProvideUserSvc(repo *persistence.UserRepository, cache *redis.Client, logger *slog.Logger) *service.UserService {
    return service.NewUserService(repo, cache, logger)
}

func ProvideUserHandler(svc *service.UserService) *http.UserHandler {
    return http.NewUserHandler(svc)
}

// Provider Set:分组
var UserSet = wire.NewSet(
    ProvideUserRepo,
    ProvideUserSvc,
    ProvideUserHandler,
)

var OrderSet = wire.NewSet(
    ProvideOrderRepo,
    ProvideOrderSvc,
    ProvideOrderHandler,
)

// Injector:声明依赖关系
func InitializeApp(cfg *config.Config, logger *slog.Logger) (*App, error) {
    wire.Build(
        ProvideDB,
        ProvideRedis,
        UserSet,
        OrderSet,
        provideRouter,
        provideServer,
        wire.Struct(new(App), "*"),
    )
    return nil, nil  // wire 会生成实际代码替换这个
}
# 生成代码
wire ./internal/app

# 生成 wire_gen.go,包含实际的依赖注入代码
// wire_gen.go(自动生成)
func InitializeApp(cfg *config.Config, logger *slog.Logger) (*App, error) {
    db, err := ProvideDB(&cfg.Database)
    if err != nil {
        return nil, err
    }
    redis, err := ProvideRedis(&cfg.Redis)
    if err != nil {
        return nil, err
    }
    userRepo := ProvideUserRepo(db)
    userSvc := ProvideUserSvc(userRepo, redis, logger)
    userHandler := ProvideUserHandler(userSvc)
    // ...
    return &App{server: server, db: db, redis: redis}, nil
}

Wire vs 手写 vs fx 对比

特性手写 DIWirefx (Uber)
依赖wire 命令行工具fx 库
方式手动构造编译时代码生成运行时反射
性能最优最优稍有开销
错误检查编译时编译时运行时
循环依赖手动避免编译时报错运行时报错
学习成本
适合项目简单/中型中型/大型大型/微服务

接口绑定(Wire)

// 接口与实现的绑定
var RepoSet = wire.NewSet(
    persistence.NewUserRepository,  // 返回 *UserRepository
    wire.Bind(new(service.UserRepository), new(*persistence.UserRepository)),
    // 将接口 service.UserRepository 绑定到 *persistence.UserRepository
)

Provider 选项

// Cleanup 函数(资源清理)
func ProvideDB(cfg *config.DatabaseConfig) (*sql.DB, func(), error) {
    db, err := sql.Open(cfg.Driver, cfg.DSN())
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        db.Close()
    }
    return db, cleanup, nil
}

// wire 会自动在生成的代码中调用 cleanup

// 接口 Provider
type Logger struct{}
func ProvideLogger() Logger { return Logger{} }

// 值绑定(用已有值作为依赖)
wire.Value(config.DefaultConfig)
wire.InterfaceValue(new(io.Reader), bytes.NewReader(nil))

依赖注入原则

原则说明
依赖接口,不依赖实现Service 依赖 Repository 接口
构造函数注入通过 New 函数参数注入
单一职责每个 Provider 只创建一个依赖
生命周期管理资源类型提供 Cleanup 函数
避免循环依赖A 依赖 B,B 不能依赖 A