依赖注入容器

依赖注入容器

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](),
        }
    },
))

也有带优先级和依赖的变体:NewLazyProviderWithPriorityNewLazyProviderWithDeps

公共 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() → reset
  • Init()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() 会:

  1. 对每个已实例化的服务,如果实现了 Destroyable,调用 Destroy()
  2. 把容器中的 entry 全部重置回未解析状态(保留 Provider 注册)

随后再次执行装配链时,所有服务都会重新 Build。这是 SIGHUP 热重载的核心机制。

推荐用法

  • 基础设施(数据库、日志、缓存) → 由 Assembly 层在 Register() 中注册
  • 业务服务(UserService、OrderService) → 在自己的 init() 函数或独立的 register.go 中通过 LazyProvider 注册,然后用 helperMustGet 取出
  • 避免在 hot path 中频繁调用 Inject(反射有开销),尽量在初始化时一次性完成
  • 避免注册大量"小对象",DI 容器最适合长生命周期的服务,不适合每次请求创建的对象