真正解耦的起点是所有构造函数参数都必须是接口类型,如userrepo、notifier、logger;接口由调用方定义,结构体实现不超过2个业务接口;wire文件需严格遵循格式规范,同类型多实例须显式绑定,本地缓存等纯内存结构无需抽象为接口。

构造函数参数必须全是接口,否则不算解耦
Go里没有“自动DI”,NewUserService接收*sql.DB或*zap.Logger就等于把实现钉死在代码里。真正解耦的起点是:所有构造函数参数都必须是接口类型,比如UserRepo、Notifier、Logger(注意不是*zap.Logger)。这样测试时才能传入fakeRepo或mockLogger,而不是被全局变量或单例卡住。
- 接口必须由调用方(如
service包)定义,不能由实现方(如repo包)“能提供什么就写什么” - 避免带事务、缓存等实现细节的方法名,比如
UpdateEmailTx()——应统一为UpdateUser(ctx, u User),让实现决定是否开事务 - 一个结构体别实现超过2个业务接口,否则mock成本高、职责模糊
wire.Build漏掉provider会导致编译报错指向错误位置
wire.Build(InitializeApp, NewService)如果漏了NewRepository,编译器不会提示“你少了一个provider”,而是直接报错:cannot use *Service as *App in return argument: *Service does not implement *App (missing Repository field)。错误信息永远指向上层结构体字段缺失,实际根源在provider链断点。
-
wire.go文件第一行必须是// +build wireinject,独占一行、无空格、无逗号、无注释;否则整个文件被静默跳过 - 该文件必须和目标函数同级:想生成
cmd/myapp/InitializeApp(),wire.go就得放在cmd/myapp/目录下 - 文件内只允许出现
func InitializeApp() *App、wire.Build(...)、_ "github.com/google/wire";混入import "fmt"或变量声明,wire直接忽略整文件
同类型多实例(如主从DB)必须显式绑定字段或值
两个函数都返回*sql.DB,wire会panic:multiple providers found for *sql.DB。它不看函数名,只认签名。靠NewPrimaryDB()和NewReplicaDB()命名解决不了问题。
- 正确做法一:
wire.Value(primaryDB)和wire.Value(replicaDB)——适用于已创建好的实例 - 正确做法二:
wire.Struct(new(App), "primaryDB", "replicaDB")——要求结构体字段名导出且明确对应 - 别指望wire推导“哪个DB是主库”,它没上下文,也不读配置;类型冲突总在生成阶段暴露,但设计时就要规划好字段契约或命名空间
本地缓存别硬套接口,map[string]string直接传值更合理
不是所有依赖都要抽象成接口。比如内存缓存用map[string]string,本身无外部依赖、无I/O、无状态管理,强行定义Cache接口再实现,反而增加间接层和mock负担。
- 优先判断:这个依赖是否需要替换(如测试换fake、生产换Redis)、是否涉及I/O或外部服务
- 纯内存数据结构、简单计算逻辑、常量配置,直接作为参数或字段传入即可
- 接口定义放在
service包,实现放在repo或infra包,避免循环引用
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











