1. 依赖注入基础:概念、方式与必要性
依赖注入(Dependency Injection,DI)是面向对象设计中实现控制反转(IoC)的核心技术手段。它通过将对象的依赖关系从内部创建转移到外部注入,从而解耦组件、提升可测试性与可维护性。本章从最基础的概念出发,逐步拆解 DI 的三种实现方式,并说明为什么现代 Go 项目(尤其是接口装配场景)离不开它。
1.1 什么是依赖注入
依赖注入的核心思想是:一个对象不应自己创建它所依赖的其他对象,而应由外部容器或框架在运行时将依赖传递进来。在 Go 中,依赖通常表现为接口或结构体指针。
关键区别:传统代码中,对象内部直接
new 或 make 依赖;DI 模式下,依赖通过参数、字段或方法传入,对象只负责使用。
下面是一个简单的对比示例:
// 传统方式:对象内部创建依赖
type UserService struct{}
func (s *UserService) GetUser(id int) *User {
repo := &UserRepository{} // 硬编码依赖
return repo.FindByID(id)
}
// DI 方式:依赖由外部注入
type UserService struct {
repo UserRepositoryInterface // 依赖接口
}
func NewUserService(repo UserRepositoryInterface) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) *User {
return s.repo.FindByID(id)
}
1.2 控制反转(IoC)概念
控制反转是 DI 背后的设计原则。它反转了谁控制依赖的创建与生命周期:
- 传统方式:高层模块主动创建低层模块,控制权在调用方。
- IoC 方式:控制权交给 IoC 容器或注入点,调用方只声明“我需要什么”,由外部决定“给什么”。
一句话记忆:“别打电话给我们,我们会打给你。”——好莱坞原则。对象不再主动寻找依赖,而是等待依赖被注入。
1.3 依赖注入的三种方式
根据依赖注入的时机与手段,业界普遍归纳为三种方式。下表从实现机制、Go 代码示例和适用场景三个维度进行对比:
| 注入方式 | 实现机制 | Go 代码示例 | 适用场景 |
|---|---|---|---|
| 构造器注入 | 通过构造函数参数传入依赖 | NewService(repo RepoInterface) *Service |
依赖必须存在,不可变;最推荐的方式 |
| Setter 注入 | 通过公开的 Setter 方法设置依赖 | s.SetRepo(repo RepoInterface) |
依赖可选,或需要在对象创建后替换 |
| 接口注入 | 依赖通过接口方法参数传入 | func (s *Service) Init(repo RepoInterface) |
框架级回调,或需要统一注入契约 |
1.3.1 构造器注入(Constructor Injection)
这是 Go 社区最推崇的方式。依赖在对象创建时一次性传入,之后不可变,保证了对象的完整性与线程安全。
// 构造器注入示例
type OrderService struct {
db DatabaseInterface
logger LoggerInterface
}
func NewOrderService(db DatabaseInterface, logger LoggerInterface) *OrderService {
return &OrderService{
db: db,
logger: logger,
}
}
// 使用
db := &PostgresDB{}
logger := &StdLogger{}
orderSvc := NewOrderService(db, logger)
优点:依赖关系在编译期可见,对象创建后状态完整,适合强制依赖。
缺点:当构造函数参数过多时,代码可读性下降(可通过引入参数对象或 Wire 解决)。
缺点:当构造函数参数过多时,代码可读性下降(可通过引入参数对象或 Wire 解决)。
1.3.2 Setter 注入(Setter Injection)
依赖通过公开的 Setter 方法在对象创建后设置。适合可选依赖或需要运行时替换的场景。
// Setter 注入示例
type CacheService struct {
cache CacheInterface // 可选依赖
}
func (s *CacheService) SetCache(cache CacheInterface) {
s.cache = cache
}
// 使用
cacheSvc := &CacheService{}
cacheSvc.SetCache(&RedisCache{}) // 创建后注入
注意:Setter 注入可能导致对象在依赖未完全注入时被使用,需配合初始化检查或延迟加载。在 Go 中,构造器注入优先于 Setter 注入。
1.3.3 接口注入(Interface Injection)
依赖通过一个专门的注入接口方法传入。这种方式在 Go 中较少直接使用,更多出现在框架级别的回调或插件系统中。
// 接口注入示例
type Injector interface {
InjectDB(db DatabaseInterface)
}
type Service struct {
db DatabaseInterface
}
func (s *Service) InjectDB(db DatabaseInterface) {
s.db = db
}
// 框架调用
var svc Service
if injector, ok := interface{}(&svc).(Injector); ok {
injector.InjectDB(&PostgresDB{})
}
Go 中的实际情况:接口注入通常被构造器注入或 Setter 注入替代。Wire 等 DI 工具主要基于构造器注入工作,因为它最符合 Go 的显式编程哲学。
1.4 为什么需要依赖注入
依赖注入不是银弹,但在中大型 Go 项目中,它解决了几个核心痛点:
- 解耦组件:高层模块不依赖低层模块的具体实现,只依赖抽象接口。替换实现时无需修改调用方代码。
- 提升可测试性:可以轻松注入 Mock 对象进行单元测试,无需启动数据库、网络等真实依赖。
- 集中管理生命周期:依赖的创建、复用、销毁由容器统一管理,避免散落在各处。
- 支持接口装配:在 Wire 等工具帮助下,通过声明式配置即可完成整个对象图的装配,减少样板代码。
实战案例:一个 Web 服务中,Handler → Service → Repository 三层架构。如果没有 DI,每层都需要手动创建下层依赖;引入 DI 后,只需在 Wire 中声明一次,所有依赖自动注入。
下表总结了有/无 DI 的典型差异:
| 维度 | 无 DI(硬编码) | 有 DI(接口注入) |
|---|---|---|
| 组件耦合度 | 高,修改实现需改调用方 | 低,只依赖接口 |
| 单元测试 | 困难,需启动真实依赖 | 容易,可注入 Mock |
| 代码复用 | 低,依赖绑定在具体实现 | 高,接口可多实现 |
| 装配复杂度 | 随组件数线性增长 | 由容器管理,复杂度可控 |
| Go 生态适配 | 与 Wire、Uber Dig 等不兼容 | 天然适配主流 DI 工具 |
本章小结:依赖注入是控制反转的具体实现,通过构造器/Setter/接口三种方式将依赖从内部创建转为外部注入。在 Go 中,构造器注入是最主流的方式,也是后续 Wire 课程的核心基础。理解 DI 的本质,是掌握接口装配的第一步。