go单元测试中应通过接口抽象依赖并手写mock控制错误返回,避免真实i/o;error判断用errors.is/as而非==或字符串匹配;panic测试需用defer+recover精准捕获。

Go 单元测试里怎么让函数返回错误
想测错误路径,核心是控制被测函数的依赖——让它在特定条件下返回 error,而不是靠真实 I/O 或网络触发失败。硬等真实错误(比如断网、磁盘满)既慢又不可控,CI 里还容易飘红。
常见错误现象:nil 指针 panic、测试始终走成功分支、mock 返回的 error 被忽略没生效。
- 用接口抽象依赖,比如把
*http.Client换成自定义HTTPDoer接口,测试时传入返回错误的 fake 实现 - 避免直接 new struct 赋值 error 字段(如
err: errors.New("boom")),而是在方法里根据输入条件返回,否则无法覆盖多分支逻辑 - 如果依赖是第三方库且没接口,用函数变量替代(如声明
var doRequest = http.DefaultClient.Do),测试前重置为返回错误的闭包
用 testify/mock 还是纯 Go 手写 mock
testify/mock 自动生成代码太重,小项目反而增加维护成本;纯 Go 手写 mock 更轻、更可控,也更容易看出“哪里被 stub 了”。
使用场景:中等规模项目、团队对 Go 接口抽象有共识、不希望引入额外构建步骤。
立即学习“go语言免费学习笔记(深入)”;
- 手写 mock 只需实现被测函数依赖的接口,方法体里按需返回
nil或具体error,比如func (m *mockDB) QueryRow(...) (*sql.Row, error) { return nil, sql.ErrNoRows } - testify/mock 适合已有大型接口且变更频繁,但要注意生成的 mock 文件要 git commit,否则 CI 构建失败
- mock 返回的 error 类型要和真实依赖一致(比如
sql.ErrNoRows而不是errors.New("not found")),否则 if err == sql.ErrNoRows 判断会失效
error 判断逻辑在测试里怎么写才可靠
用 == 直接比较 error 值只适用于导出的哨兵错误(如 io.EOF),自定义错误或包装错误(fmt.Errorf("wrap: %w", err))必须用 errors.Is 或 errors.As。
性能影响:两者开销极小,但错用会导致测试误通过或误失败,比性能问题更致命。
- 检查是否为某类错误:
if !errors.Is(err, os.ErrNotExist) { t.Fatal("expected os.ErrNotExist") } - 检查是否包含底层错误:
var pathErr *os.PathError; if errors.As(err, &pathErr) { ... } - 避免写
if err.Error() == "file not found"—— 字符串匹配脆弱,且无法处理 error 包装
panic 和 error 混用时测试怎么覆盖
Go 函数里混用 panic 和 error 是反模式,但存量代码常有。测试 panic 必须用 recover 捕获,不能靠 defer + log。
容易踩的坑:recover 写在错误调用外层、没重置 goroutine 状态、panic 信息格式和预期不一致。
- 正确姿势:用
defer func() { if r := recover(); r != nil { /* 检查 r */ } }()包裹被测调用 - 如果被测函数在 goroutine 里 panic,主 goroutine 的 recover 捕不到,得在内部加 recover 或改用 channel 同步信号
- panic 值可能是 string、error 或自定义 struct,别假设一定是
error;用reflect.TypeOf(r).Kind() == reflect.String兜底判断
事情说清了就结束。真正难的不是写 mock,而是把依赖拆干净、让 error 能被构造、让 panic 有迹可循——这些设计决策,往往在第一次写函数时就定下了。










