依赖注入
为什么需要依赖注入
// ❌ 硬编码依赖:紧耦合,难以测试
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 对比
| 特性 | 手写 DI | Wire | fx (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 |