Go 依赖注入核心是接口抽象、显式传参和集中组装;手动 DI 优于反射自动填充,中小项目应手写 New 函数与 app.NewApp 组装链;接口需最小化、导出且按调用方定义;运行时容器仅适用于插件化场景;第三方框架需权衡代价。

Go 语言里没有“必须用框架才算依赖注入”这回事。真正有效的依赖注入,是把 接口抽象、构造函数参数显式传入 和 依赖组装集中可控 三者结合——不是靠反射自动填值,而是靠人写清楚谁依赖谁。
手动 DI:先写 New 函数,再考虑是否自动化
中小型项目或新服务起步阶段,直接手写初始化链最清晰:
- 每个组件只暴露
NewXxx(...)函数,参数全是接口(如Logger、UserRepo),不接受具体实现(如*zap.Logger) - 把所有初始化逻辑抽到一个
app.NewApp(cfg Config)中,自底向上创建:先连 DB、再建 Repo、最后组 Service - main 函数是唯一合法的组装点,这里出错堆栈直指根源;避免分散在 init 或各包 New 里,否则 panic 时找不到源头
接口设计:按调用方需要定义,而非实现方能提供什么
接口不是装饰品,它决定测试和替换的边界:
- 接口必须导出(首字母大写),且方法集最小化——只包含当前调用方真正用到的方法,比如
UserRepo只需GetByID(ctx, id)和Save(ctx, u),别塞进UpdateEmailTx - 一个结构体别实现太多接口,否则违反单一职责,mock 成本高、语义模糊
- 别为 DI 强行加接口:本地缓存
map[string]string、简单工具函数等无替换需求的类型,直接传值更干净
运行时容器:仅当插件化或热加载真有需要时才手写
若你做的是支持用户上传 Handler 的网关、可动态加载 Repository 的 CLI 工具,那轻量容器才有意义:
立即学习“go语言免费学习笔记(深入)”;
- 用
struct { Type reflect.Type; Name string }当 map key,支持同类型多实例(如*sql.DB命名为 "primary" 和 "analytics") - 缓存层用
sync.Map存已构建完成的实例;构造函数(func() interface{})存在普通 map,避免闭包 GC 压力 - 提供
GetOrBuild()而非Get(),首次调用才执行构造逻辑,后续直取缓存 - 禁止在构造函数内调用
container.Get(),必须在Provide()阶段就做依赖图环检测(可用拓扑排序)
第三方框架:看清代价再选,别被“自动化”带偏
Wire、Dig、Fx 不是银弹,它们解决的问题很具体:
-
Wire:适合依赖稳定、强调编译期检查和零运行时开销的上线服务;但不支持运行时注册,泛型类型需 bind 显式声明,改依赖就得
wire build -
Dig:适合快速原型,但错误堆栈指向内部 reflect,
interface{} is not registered这类 panic 很难定位到哪行Get()漏了注册 -
Fx:启动阶段同步执行构造函数,DB 连接、HTTP client 初始化若放里面,整个应用会卡住;
fx.Provide必须传函数,不能传已实例化对象


















