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:
非常欢迎反馈——尤其是关于设计权衡和边缘情况的意见。