Go依赖注入核心是将NewXXX函数改为仅接收依赖参数、不创建依赖,结构体字段用interface抽象,校验nil,接口最小化;测试时传fake或mock,main为唯一组装点。

把依赖从 new 里抽出来,改用参数传入
Go 默认构造函数常直接初始化外部依赖(比如 sql.Open、redis.NewClient),这会让测试时不得不启动真实服务。真正要做的不是“加 mock”,而是让 NewXXX 函数只负责组装,不负责创建。
例如原写法:
func NewUserService() *UserService {
db, _ := sql.Open("sqlite", ":memory:")
cache := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
return &UserService{db: db, cache: cache}
}
改成:
func NewUserService(db *sql.DB, cache *redis.Client) *UserService {
return &UserService{db: db, cache: cache}
}
- 所有外部依赖都变成函数参数,业务逻辑不变,只是解耦了初始化时机
- 测试时可直接传入
sqlmock.New()或内存版redis实例,零配置绕过网络和磁盘 - 如果某些依赖是可选的(如 metrics 客户端),用结构体字段 + 选项函数(functional option)更灵活
用 interface 抽象关键交互点,别依赖具体类型
Go 的接口不是装饰,是测试的基础设施。只要某段逻辑会调用外部服务,就该定义最小接口,而不是直接用 *http.Client 或 *sql.DB。
立即学习“go语言免费学习笔记(深入)”;
比如一个发通知的服务:
- ❌ 错误做法:函数签名写
func SendEmail(c *smtp.Client, to, body string) - ✅ 正确做法:定义
type EmailSender interface { Send(to, body string) error },然后函数接收该接口
这样测试时就能用一个简单 struct 实现该接口,连 import 都不用加:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type fakeEmailer struct{}
func (f fakeEmailer) Send(to, body string) error { return nil }
注意:接口要够小——只包含当前函数实际用到的方法,否则会增加实现负担,也违背“最小契约”原则。
避免 time.Now()、rand.Intn() 这类不可控调用嵌在业务逻辑里
这些函数一调用,测试就失去确定性。不是不能用,而是得把它变成可注入的依赖。
- 定义
type Clock interface { Now() time.Time },默认实现用time.Now,测试时换成固定时间 - 对随机数,用
type Rand interface { Intn(n int) int },生产用rand.Rand,测试用预设种子的实例 - 千万别在函数内部直接调用
time.Sleep——超时控制应由调用方决定,或通过context.WithTimeout传递
这类替换几乎不改变原有逻辑分支,只是把“谁来提供时间/随机数”这个决策上移,测试时自然获得完全可控的输入。
internal 包里的私有逻辑怎么测?别跨包,就地写 test 文件
很多人想把测试放到 test/ 目录下,结果发现访问不了 parseConfig() 这种未导出函数——Go 不允许跨包访问私有标识符,这是设计,不是 bug。
正确姿势只有一个:
- 确保
internal/handler/handler.go和internal/handler/handler_test.go在同一目录、同包名(package handler) - 测试文件里可以直接调用
parseQuery()、buildURL()等私有函数,无需导出 - 如果需要共享测试工具(比如造数据的
newTestDB()),放在同目录下的testutil.go里,并加//go:build unit标签隔离构建
真正容易被忽略的是:模块根目录(含 go.mod)才是 go test 的工作基准。在 internal/handler 目录下运行 go test 可能因路径解析失败而找不到依赖,务必回到项目根目录执行。

















