应使用接口定义契约而非具体类型传参,业务代码需依赖窄接口(如userrepository),由生产实现和测试mock共同满足;接口须定义在调用方或共享包中,方法签名克制,依赖须显式注入,避免全局变量和init初始化,http handler测试应拆分逻辑与完整流程并注入mock依赖。

用接口定义契约,而不是用具体类型传参
测试环境与生产环境依赖隔离失败,90% 的根源是业务代码直接依赖了具体实现类型,比如 mysql.DB、redis.Client 或自定义的 ConnPool 结构体。Go 编译器不会允许你在测试中把 *MockDB 塞进一个只接受 *sql.DB 的函数参数里——它不看“像不像”,只认“是不是”。
正确做法是:在业务模块(如 service/ 或独立 interfaces/ 包)中定义窄接口,只暴露当前需要的方法:
type UserRepository interface {
GetUserByID(id int) (*User, error)
SaveUser(*User) error
}
然后让生产实现和测试 mock 都去满足这个接口,而不是反过来让业务逻辑去适配某个 SDK 类型。
- 接口必须定义在调用方或共享 contract 包里,**不能放在
mysql或redis实现包内**——否则测试时仍得导入生产依赖 - 方法签名要克制:不要定义
BeginTx、Close这类仅测试不需要的“全量能力” - 如果已有第三方类型(如
*gorm.DB)必须复用,可通过包装器(wrapper)或嵌入方式桥接,而非强转
依赖注入必须显式传递,避免全局变量或 init 初始化
常见错误是把 DB 客户端声明为包级变量,再在 init() 里初始化:
var DB *gorm.DB
func init() {
DB = gorm.Open(...) // 测试无法替换
}
这导致测试时无法注入 mock,也无法控制生命周期。所有外部依赖都应作为构造函数参数传入:
type UserService struct {
repo UserRepository // 接口,非具体类型
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
- 构造函数接收接口,业务逻辑只调用接口方法,完全不感知底层是什么
- 测试时直接传入
&MockUserRepo{};生产启动时才创建真实&MySQLUserRepo{db: realDB} - 避免使用
context.WithValue或配置中心动态切换实现——那只是把耦合从代码移到了运行时,没解决可测性问题
测试中用组合代替继承,手写 Mock 更可控
第三方 mock 工具(如 gomock、mockery)生成的代码往往冗长、难调试,且容易因接口微调而大面积报错。对简单接口,手写 mock 更轻量、意图更清晰:
type MockUserRepo struct {
Users map[int]*User
}
func (m *MockUserRepo) GetUserByID(id int) (*User, error) {
u, ok := m.Users[id]
if !ok {
return nil, errors.New("not found")
}
return u, nil
}
func (m *MockUserRepo) SaveUser(u *User) error {
m.Users[u.ID] = u
return nil
}
- 无需额外工具链,不引入构建依赖,
_test.go文件里直接定义即可 - 可自由添加状态字段(如
CallCount、LastArgs)用于断言行为 - 注意:mock 结构体方法接收者要用指针(
*MockUserRepo),否则无法满足接口(值类型方法集 ≠ 指针类型方法集)
HTTP handler 测试必须拆开 net/http 生命周期
别用 http.ListenAndServe 测试 handler,也不要在测试里起真实服务。Go 的 httptest 提供两套正交方案:
- 测单个 handler 函数逻辑:用
httptest.NewRequest+httptest.NewRecorder,快且精准,但不经过中间件 - 测完整 HTTP 流程(含中间件、路由、header、重定向):用
httptest.NewServer,它自动分配空闲端口,返回*httptest.Server,记得defer server.Close() - handler 内部若依赖外部服务(如数据库),必须通过结构体字段注入接口实例,测试时替换为 mock,而不是在 handler 里硬编码调用
最容易被忽略的是:handler 构造函数本身就要接收依赖接口,而不是在函数体内现场 new 实例——否则测试永远绕不开真实依赖。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











