依赖注入
依赖注入
依赖注入是把对象依赖的组件从外部传入,而不是在对象内部直接创建。Go 通常使用构造函数显式注入;只有依赖图变得复杂时,才需要引入工具。
| 方式 | 注入时机 | 特点 | 适用场景 |
|---|---|---|---|
| 手动组装 | 编译期 | 无额外依赖,调用关系最直观 | 小型和中型项目 |
Wire | 代码生成 | 生成普通 Go 代码,不使用反射 | 已使用 Wire 的存量项目 |
Dig | 运行时 | 基于反射构建依赖容器 | 需要动态组装依赖的库或框架 |
Fx | 运行时 | 基于 Dig,并管理应用生命周期和模块 | 组件较多的长期运行服务 |
新项目应先考虑手动组装。Wire 已于 2025 年 8 月归档且不再维护,不建议在新项目中引入;需要容器时,通常直接选择 Fx,而不是在业务代码中使用 Dig。
下面的示例使用相同的构造函数:
type Repository struct {
db *sql.DB
}
func NewRepository(db *sql.DB) *Repository {
return &Repository{db: db}
}
type Service struct {
repo *Repository
}
func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
手动组装
在程序入口按依赖顺序调用构造函数,依赖关系可以由编译器检查。
func main() {
db, err := NewDB()
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo := NewRepository(db)
service := NewService(repo)
run(service)
}
当初始化代码仍然清晰时,手动组装通常是最简单的选择。
Wire
Wire 根据 Provider 的参数和返回值生成组装代码。Injector 文件通常使用构建标签隔离:
//go:build wireinject
package main
import "github.com/google/wire"
func InitializeService() (*Service, error) {
wire.Build(NewDB, NewRepository, NewService)
return nil, nil
}
安装并运行生成器:
go install github.com/google/wire/cmd/wire@latest
wire
Wire 会生成包含实际构造调用的 wire_gen.go。生成结果应提交到版本控制,使未安装 Wire 的环境也能正常构建。
Dig
Dig 使用 Provide 注册构造函数,再通过 Invoke 解析并使用依赖。
container := dig.New()
container.Provide(NewDB)
container.Provide(NewRepository)
container.Provide(NewService)
err := container.Invoke(func(service *Service) {
run(service)
})
缺少依赖、重复提供同一类型或出现循环依赖时,错误会在容器解析依赖图时暴露,而不是由编译器直接指出。应把容器限制在程序入口,避免将它作为 Service Locator 传入业务代码。
Fx
Fx 在 Dig 之上提供模块、启动和停止钩子,以及操作系统信号处理。
func main() {
fx.New(
fx.Provide(
NewDB,
NewRepository,
NewService,
),
fx.Invoke(run),
).Run()
}
需要管理资源生命周期时,通过 fx.Lifecycle 注册钩子:
func NewServer(lc fx.Lifecycle, service *Service) *http.Server {
server := &http.Server{Addr: ":8080"}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go server.ListenAndServe()
return nil
},
OnStop: func(ctx context.Context) error {
return server.Shutdown(ctx)
},
})
return server
}
选择建议
- 初始化关系简单时使用手动组装,不要为了形式统一而引入容器。
- 已有 Wire 项目可以继续使用生成代码,但应评估迁移路径。
- 只需要依赖容器时使用 Dig,并将容器控制在组合根中。
- 需要模块化、生命周期管理和优雅退出时使用 Fx。
- 无论选择哪种方式,构造函数都应保持可独立调用,业务逻辑不应依赖具体的注入框架。