依赖注入容器
依赖注入容器
pkg/container/ 实现了一个轻量但完备的依赖注入容器。它是 go-web 中所有基础设施服务(数据库、日志、缓存、配置……)共享的来源,理解它有助于你扩展框架或编写可测试的业务代码。
设计目标
- 类型即标识 —— 服务通过
reflect.Type索引,而不是字符串名称 - 懒加载单例 —— 第一次
Get时才执行Build(),失败可重试 - 优先级覆盖 —— 同类型可注册多个 Provider,高优先级覆盖低优先级
- 生命周期钩子 —— 实现
Initializable/Destroyable即可参与 init / destroy 阶段 - 拓扑排序 —— 通过
DependencyAware.DependsOn()声明依赖,容器按正确顺序初始化和销毁 - 运行时循环检测 —— 通过 goroutine ID 跟踪解析链,出现循环时立刻报错
三个核心接口
type Provider interface {
Type() reflect.Type // 服务类型标识
Build() any // 构建服务实例
Priority() int // 优先级(同类型多实现时生效)
}
type Initializable interface {
Init() error // 解析后立即调用
}
type Destroyable interface {
Destroy() error // 重置/销毁时调用
}
// 可选:声明依赖
type DependencyAware interface {
DependsOn() []reflect.Type
}内置 Provider 实现
容器提供两种开箱即用的 Provider 包装,你几乎不需要自己实现 Provider 接口。
SimpleProvider —— 包装已有实例
container.Register(container.NewSimpleProvider(
reflect.TypeFor[*MyService](),
&MyService{Name: "demo"},
))
// 带优先级
container.Register(container.NewSimpleProviderWithPriority(
reflect.TypeFor[*MyService](),
&MyService{Name: "demo"},
100,
))
// 声明依赖
container.Register(container.NewSimpleProviderWithDeps(
reflect.TypeFor[*MyService](),
&MyService{},
reflect.TypeFor[*Database](),
reflect.TypeFor[gsr.Logger](),
))LazyProvider —— 延迟创建
适合需要在 Build 时调用其他服务的场景:
container.Register(container.NewLazyProvider(
reflect.TypeFor[*UserService](),
func() any {
return &UserService{
DB: container.MustGet[*gorm.DB](),
Logger: container.MustGet[gsr.Logger](),
}
},
))也有带优先级和依赖的变体:NewLazyProviderWithPriority、NewLazyProviderWithDeps。
公共 API
下面这些函数都是 pkg/container 包的导出顶层函数,内部委托给全局容器(var global = NewContainer())。
| 函数 | 说明 |
|---|---|
Register(p Provider) |
注册 Provider |
Get[T any]() (T, error) |
按类型获取服务 |
MustGet[T any]() T |
同上,失败 panic |
Inject(target any) error |
反射注入结构体字段(inject:"" 标签) |
Reset(typ reflect.Type) |
重置单个服务实例(下次 Get 重新 Build) |
ResetAll() |
重置所有实例(SIGHUP 热重载使用) |
ListProviders() []string |
列出所有已注册 Provider 的类型名 |
InitAll() error |
按拓扑序初始化所有 Provider |
DestroyAll() error |
按反向拓扑序销毁所有实例 |
注意:
Get[T]()与MustGet[T]()是 Go 1.18+ 的泛型函数,不需要传reflect.Type参数;直接container.MustGet[*gorm.DB]()即可。
反射注入
通过 inject:"" 标签按字段类型自动注入,inject:"-" 跳过:
type UserController struct {
DB *gorm.DB `inject:""`
Logger gsr.Logger `inject:""`
cache string `inject:"-"` // 跳过
}
ctrl := &UserController{}
if err := container.Inject(ctrl); err != nil {
panic(err)
}
// ctrl.DB / ctrl.Logger 已被填充注意:
- 目标必须是
*Struct(指针指向结构体) - 字段必须可导出(首字母大写)且类型与某个已注册 Provider 完全匹配
- 注入会触发对应 Provider 的懒加载
优先级覆盖
当同一类型被多次注册时,只有 Priority() 更高的 Provider 生效;相同优先级的会被忽略。这使得在测试或扩展场景下,无需修改原有装配代码就能替换默认实现:
// 默认装配(priority=0)
container.Register(container.NewSimpleProvider(
reflect.TypeFor[gsr.Logger](),
productionLogger,
))
// 测试时覆盖(priority=100)
container.Register(container.NewSimpleProviderWithPriority(
reflect.TypeFor[gsr.Logger](),
mockLogger,
100,
))生命周期
register → resolve(首次 Get) → Init() → ...运行... → Destroy() → resetInit()在Build()完成后立即被调用,返回 error 时整次 resolve 失败,下次Get会重试Destroy()在Reset(typ)/ResetAll()/DestroyAll()时被调用InitAll()与DestroyAll()会通过拓扑排序保证顺序正确
循环依赖检测
容器在每次 resolve 时通过 goroutine ID 跟踪「正在解析的类型集合」。如果同一调用链中再次出现同一个类型,立刻报错:
circular dependency detected at runtime: *foo.A is already being resolved in this call chain此外 InitAll / DestroyAll 还会对所有 Provider 做静态拓扑排序,在启动阶段就能发现声明式的循环依赖。
热重载
ResetAll() 会:
- 对每个已实例化的服务,如果实现了
Destroyable,调用Destroy() - 把容器中的 entry 全部重置回未解析状态(保留 Provider 注册)
随后再次执行装配链时,所有服务都会重新 Build。这是 SIGHUP 热重载的核心机制。
推荐用法
- 基础设施(数据库、日志、缓存) → 由 Assembly 层在
Register()中注册 - 业务服务(UserService、OrderService) → 在自己的
init()函数或独立的register.go中通过LazyProvider注册,然后用helper或MustGet取出 - 避免在 hot path 中频繁调用
Inject(反射有开销),尽量在初始化时一次性完成 - 避免注册大量"小对象",DI 容器最适合长生命周期的服务,不适合每次请求创建的对象