Go依赖注入应靠interface+构造函数注入+明确分层边界,而非框架自动扫描或反射;硬编码new或全局变量会导致测试难替换、违反依赖倒置、修改扩散、循环import等问题,且接口定义须由调用方驱动、契约精准、避免泛化。

直接结论:靠 interface + 构造函数注入 + 明确分层边界,而不是靠框架自动扫描或反射。
为什么不能用 new() 或全局变量管理依赖
硬编码 new(UserService) 或在 init 函数里初始化 db、redisClient,会导致:
- 单元测试时无法替换依赖,必须启动真实 MySQL/Redis
- Service 层直接 import 数据库驱动(如
"github.com/go-sql-driver/mysql"),违反依赖倒置原则 - 新增一个缓存策略(比如从 Redis 换成 Badger),要改所有调用点,而非只换一个实现
- 模块间出现循环 import:A 依赖 B,B 又间接依赖 A 的某个结构体
interface 定义必须紧贴调用方需求
不是“先写好数据库 CRUD 接口再套业务”,而是由 Service 层提出它真正需要的能力。例如订单创建不需要 UpdateUserStatus,但需要 LockInventory 和 RecordPaymentEvent。
- Repository 接口定义在
internal/service/order下,而非internal/repository—— 调用方决定契约 - 避免泛化接口如
CRUDer,它会让实现被迫支持无关操作,破坏单一职责 - 方法参数尽量用 struct(如
*CreateOrderRequest),不用多个 string/int 参数,便于后续扩展字段
构造函数注入比 Wire 更可控(尤其初期)
Wire 适合稳定的大项目,但小团队或快速迭代阶段,手动构造函数更易 debug、更少隐式行为。
立即学习“go语言免费学习笔记(深入)”;
- 每个 Service 的
NewXXXService函数只接收它真正依赖的 interface,不传整个 config 或 ctx - 注入链清晰:cmd/main.go → NewApp() → NewOrderService(db, mq, payment) → NewInventoryRepo(db)
- 禁止跨层传递实现:Handler 层不能持有
*sql.DB,只能通过 Service 层暴露的 interface 调用 - 若用 Wire,Provider 函数返回类型必须是 interface(如
func() UserRepository),不能返回*mysql.UserRepo
go.mod 里禁止间接依赖污染主模块
第三方 SDK(如支付网关 client)不该被 service 层直接 import,否则会把它的依赖(比如某个 HTTP 库的旧版 golang.org/x/net)带进整个服务。
- 在
internal/infra/payment封装一层适配器,对外只暴露PaymentClientinterface - 该目录下的
go.mod独立管理支付 SDK 版本,主模块只依赖 interface - 运行
go list -m all | grep payment,确认主模块的go.mod中没有支付 SDK 的直接 require 条目 - 如果 SDK 有 breaking change,只需重写适配器,不碰业务逻辑
最常被忽略的一点:解耦不是一次性动作,而是每次新增功能时都得问——这个依赖,是不是真的要流到这一层?还是应该被挡在 infra 或 adapter 里?


















