Go语言:使用Wire实现依赖注入

商城项目用Go开发,依赖关系一复杂就头疼。Java有Spring自动注入,Go社区的答案是Wire——Google出品的编译时依赖注入工具。

Go为什么需要DI

Go没有注解、没有反射式框架(不推荐用reflect做DI)。项目小的时候,手动在main里new一堆struct然后层层传递还能忍。但当你有20个service、每个service依赖3-5个repo和其他service的时候,main函数会变成这样:

db := initDB()
redis := initRedis()
userRepo := repo.NewUserRepo(db)
productRepo := repo.NewProductRepo(db)
orderRepo := repo.NewOrderRepo(db)
inventoryRepo := repo.NewInventoryRepo(db)
cacheService := service.NewCacheService(redis)
userService := service.NewUserService(userRepo, cacheService)
productService := service.NewProductService(productRepo, inventoryRepo, cacheService)
orderService := service.NewOrderService(orderRepo, productService, userService)
// ... 还有二十行

维护噩梦。加一个依赖要改好几个地方。Wire就是解决这个问题的。

Wire的核心概念

Wire是编译时DI,不是运行时。它通过代码生成来完成注入,没有反射开销,类型安全。

Provider

Provider就是一个普通的Go函数,接受依赖作为参数,返回一个实例:

// 这就是一个Provider
func NewUserService(repo *UserRepo, cache *CacheService) *UserService {
    return &UserService{repo: repo, cache: cache}
}

ProviderSet

把相关的Provider打包:

var UserSet = wire.NewSet(
    NewUserRepo,
    NewUserService,
)

var ProductSet = wire.NewSet(
    NewProductRepo,
    NewInventoryRepo,
    NewProductService,
)

Injector

Injector是你声明"我需要什么"的地方。写一个函数签名,Wire自动生成实现:

//go:build wireinject

func InitializeApp(cfg *config.Config) (*App, error) {
    wire.Build(
        // 基础设施
        InitDB,       // func(*config.Config) (*sql.DB, error)
        InitRedis,    // func(*config.Config) (*redis.Client, error)

        // 业务模块
        UserSet,
        ProductSet,
        OrderSet,

        // 应用
        NewApp,
    )
    return nil, nil // Wire会替换这个返回值
}

运行wire ./cmd/server/,Wire分析依赖图,生成wire_gen.go

// Code generated by Wire. DO NOT EDIT.
func InitializeApp(cfg *config.Config) (*App, error) {
    db, err := InitDB(cfg)
    if err != nil {
        return nil, err
    }
    redisClient, err := InitRedis(cfg)
    if err != nil {
        return nil, err
    }
    userRepo := repo.NewUserRepo(db)
    cacheService := service.NewCacheService(redisClient)
    userService := service.NewUserService(userRepo, cacheService)
    // ... 自动排好顺序
    app := NewApp(userService, productService, orderService)
    return app, nil
}

编译时就确定了所有依赖关系,缺啥直接报编译错误,不会等到运行时才炸。

实际项目中的用法

按模块组织Provider

// internal/user/provider.go
package user

var ProviderSet = wire.NewSet(
    repo.NewUserRepo,
    service.NewUserService,
    handler.NewUserHandler,
)
// internal/product/provider.go
package product

var ProviderSet = wire.NewSet(
    repo.NewProductRepo,
    repo.NewInventoryRepo,
    service.NewProductService,
    handler.NewProductHandler,
)

接口绑定

如果Provider返回的是实现类型,但依赖方需要接口:

var ProviderSet = wire.NewSet(
    NewMySQLUserRepo,
    // 告诉Wire:*MySQLUserRepo 实现了 UserRepository 接口
    wire.Bind(new(UserRepository), new(*MySQLUserRepo)),
)

配置分组

wire.Struct自动注入struct的字段:

type AppConfig struct {
    DB    *DBConfig
    Redis *RedisConfig
    Auth  *AuthConfig
}

var ConfigSet = wire.NewSet(
    LoadDBConfig,
    LoadRedisConfig,
    LoadAuthConfig,
    wire.Struct(new(AppConfig), "*"), // 自动填充所有字段
)

常见问题

Q: Wire和uber/fx有什么区别?
Wire是编译时生成代码,fx是运行时反射注入。Wire更Go-style(类型安全、无反射),fx更灵活(支持生命周期管理)。我选Wire因为编译时检查更安全。

Q: 循环依赖怎么办?
Wire会直接报错。解决方案和其他DI框架一样——通过接口解耦或重新设计模块边界。

Q: Provider可以返回error吗?
可以。Wire会自动处理error传播,如果某个Provider返回了error,整个注入链会正确地往上传递。

Wire用起来有一点学习成本,但在中大型Go项目里绝对值得。比手写依赖初始化代码清晰太多了。