Go 中的依赖注入,简化为字段标签

发布: (2025年12月18日 GMT+8 09:47)
4 min read
原文: Dev.to

Source: Dev.to

核心思想

所有依赖注入都通过结构体字段标签声明。
除此之外别无他物。

  • 没有 provider 集合。
  • 没有 DSL。
  • 没有运行时反射。

容器声明 注入什么
Provider 声明 如何构造 值。

容器

容器仅仅是一个结构体。带有 inject 标签的字段由注入器管理。

type Container struct {
    UserService service.UserService `inject:""`
}

inject 标签刻意保持最小化:

  • 它仅是一个标记。
  • 默认不携带任何配置。
  • 它仅表示 “该字段由注入器注入”。

Provider

只要是返回值的顶层函数都可以称为 provider。

func NewUserService(db infra.Database) service.UserService {
    return &userService{DB: db}
}

规则很简单:

  • 函数必须是顶层的(没有接收者)。
  • 参数即为依赖。
  • 返回值即为提供的类型。

注入器通过静态分析自动发现 provider。

生成的代码

运行生成器:

injector generate ./...

它会生成普通的 Go 代码:

func NewContainer() *Container {
    cfg := NewDatabaseConfig()
    db := NewDatabase(cfg)
    user := NewUserService(db)

    return &Container{
        UserService: user,
    }
}

没有运行时的魔法。生成的代码可读、可调试且类型安全。

默认接口优先

注入器天然支持接口。

type UserService interface {
    Register(name, password string) error
}

func NewUserService(db infra.Database) UserService {
    return &userService{DB: db}
}

容器只暴露接口。具体实现保持私有,从而在不引入 DI 专用抽象的情况下保持应用边界的清晰。

处理多个 Provider

如果有多个 provider 返回相同的类型,注入器要求显式选择。

type Container struct {
    _ config.DatabaseConfig `inject:"provider:NewPrimaryDatabaseConfig"`
    UserService service.UserService `inject`
}

空白(_)字段:

  • 不会暴露该依赖。
  • 声明一个 provider 覆盖。
  • 在容器内部全局生效。

Provider 的选择保持集中且显式。

为什么使用字段标签?

Go 的结构体本身就代表了依赖列表。再添加额外的配置层往往会让 DI 更难以推理,而不是更容易。

通过把 DI 声明限制在字段标签上:

  • 依赖一目了然。
  • 配置保持局部。
  • 心智模型保持简洁。

DI 应该是基础设施,而不是代码库的核心关注点。

状态

Injector 故意保持小而有主见。当前目标不是功能的广度,而是清晰度:

  • 基于标记的容器。
  • 基于 provider 的解析。
  • 编译时依赖图。

未来还有很多想法,但可以等到真实使用场景推动时再实现。

链接

GitHub:

非常欢迎反馈——尤其是关于设计权衡和边缘情况的意见。

Back to Blog

相关文章

阅读更多 »