依赖注入起点是构造函数参数全为接口,如UserRepo而非*sql.DB;接口由调用方定义、职责清晰;Wire配置需严格遵循格式规范,测试应手写fake而非依赖Wire。

构造函数参数全是接口,才是依赖注入的起点
Go 里没有“自动 DI”,NewUserService 接收 *sql.DB 就不算——它绑死了实现;接收 UserRepo(接口)才真正开始解耦。接口必须由调用方定义,不是实现方“能提供什么就写什么”。比如 UserRepo 不该有 UpdateEmailTx() 这种带事务细节的方法,而应是 UpdateUser(ctx, u User),让具体实现决定是否开事务。
常见错误现象:go test 报 nil pointer dereference,一查发现 NewUserService 里直接用了全局变量 db,或者构造函数参数是 *zap.Logger 而非 Logger 接口——测试时根本没法传 fake 实现。
- 接口定义放在 service 包,实现放在 repo 包,避免循环引用
- 一个结构体别实现超过 2 个业务接口,否则 mock 成本高、职责模糊
- 本地缓存用
map[string]string?直接传值,别硬套接口
wire.go 第一行写错,整个生成就静默失效
// +build wireinject 必须独占第一行,前面不能有空格,后面不能加逗号或注释。漏了这行,wire build 和 go build 都会跳过该文件,不报错、不警告、不生成代码——你改完所有 provider 函数,InitializeApp 就是不存在。
这个文件还必须和目标包同级:你要生成 cmd/myapp/InitializeApp(),wire.go 就得放在 cmd/myapp/ 目录下,不能扔在根目录或 internal/ 里。
立即学习“go语言免费学习笔记(深入)”;
- 文件里只允许
func InitializeApp() *App、wire.Build(...)、_ "github.com/google/wire" - 混入
import "fmt"或变量声明,wire直接忽略整文件 -
wire.Build(InitializeApp, NewService)漏了NewRepository?编译报错永远指向NewService缺*Repository,不会提示“你漏了 NewRepository”
同类型多实例(如主从 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 是主库”——它没上下文,也不读配置。类型冲突报错总在生成阶段,但根源是设计时没提前规划好命名空间或字段契约。
测试时别碰 wire,手写 fake 更快更稳
单元测试里用 wire 是自找麻烦。你只是想验证 UserService.GetUser 是否调用了 repo,结果还要配 wire.Build、处理生成失败、调试 inject.go 里的反射调用栈。
直接在 service_test.go 里写一个 fakeUserRepo:
type fakeUserRepo struct {
users map[int]User
}
func (f *fakeUserRepo) GetUser(id int) (User, error) {
u, ok := f.users[id]
if !ok {
return User{}, errors.New("not found")
}
return u, nil
}
然后 NewUserService(&fakeUserRepo{users: map[int]User{1: {ID: 1}}}) ——三行搞定,无依赖、无生成、断点一步到位。
复杂点在于:很多人把 fake 写成全局单例,忘了并发测试时状态污染;更隐蔽的是,fake 实现里偷偷用了真实 DB 连接——这种“半 fake”比全真实还难 debug。


















