依赖注入是将对象依赖关系从代码中解耦并由外部统一管理的设计模式,核心是不自行new、不硬编码依赖,通过接口抽象、构造函数注入、容器注册与解析实现松耦合与可测试性。

依赖注入不是“自动填参数”的魔法,而是把对象之间谁该用谁、谁该管谁的问题,从代码里拎出来统一安排。核心就一点:不自己 new,也不硬写依赖名,让外部决定给什么实例。
明确接口与实现分离
先定义行为契约,再提供具体实现。比如用户操作需要发通知,就先写一个 Notifier 接口:
- 定义
Send(message string) error方法 - 写
EmailService和SmsService两个结构体,各自实现 Notifier - 业务逻辑层(如
UserService)只依赖 Notifier 接口,不关心是邮件还是短信
用构造函数注入强制声明依赖
把必需的协作对象作为参数放进构造函数,既清晰又安全:
-
func NewUserService(n Notifier) *UserService—— 明确表达“没通知器我没法干活” - 避免字段注入或 setter 注入带来的空指针风险和状态不一致问题
- 测试时可直接传入 mock 实现,无需改源码或启动容器
交由容器统一管理生命周期
手工 new 每个依赖容易出错且难维护。现代框架(如 FastAPI 的 Depends、Spring 的 @Service、.NET 的 IServiceCollection)都提供服务注册与解析能力:
- 把
EmailService{}注册为 Notifier 类型的瞬态/单例实例 - 声明
UserService构造时需要 Notifier,容器自动匹配并注入 - 不同环境可注册不同实现(开发用 ConsoleNotifier,生产用 EmailService)
避免隐式依赖和过度自动装配
自动装配(如 Spring 的 @Autowired 或 FastAPI 的依赖嵌套)虽方便,但容易掩盖真实依赖链:
- 优先显式写出构造参数,而非靠反射猜;注解只是辅助,不是替代设计
- 警惕循环依赖:A 依赖 B,B 又依赖 A —— 容器通常报错,说明设计有紧耦合
- 接口粒度要合理:别为一个方法建一个接口,也别让一个接口塞十几种行为
真正解耦的关键不在工具,而在每次写 new 前多问一句:这个对象的创建逻辑,真的属于当前类的职责吗?










