基准测试中b.resettimer()必须放在一次性setup操作之后、循环体之前,否则会将准备阶段耗时计入性能指标。

怎么一眼看出哪个测试拖慢了整体速度
别靠猜,直接用 go test -v -bench=. -benchmem -timeout=5m 加上 -v 输出详细日志,观察每个测试的耗时。更准的办法是加 -run=^Test.*$ -args -test.bench=. 混合运行,但最实用的是:先跑一次全量,记下总时间;再用 go test -run=TestFoo 单独跑疑似慢的测试,对比是否显著偏高。注意,有些测试看似快,但因阻塞 goroutine 或未关闭资源(如 http.Server 未 Shutdown),会拖慢后续所有测试——这种“隐形慢”得靠 pprof 抓。
为什么 t.Parallel() 有时反而让测试更慢甚至失败
t.Parallel() 不是银弹,它只对真正独立、无共享状态的测试有效。常见翻车点:
- 测试里读写了全局变量或包级变量(比如
config := loadConfig()放在函数外) - 多个测试共用同一个内存数据库实例,没做事务隔离或重置(比如 sqlite in-memory db 被反复写入)
- 用了
time.Sleep或依赖系统时钟,而并行后时序错乱 - 并发数超过 CPU 核心数太多,反而触发调度开销(尤其在 CI 机器上资源受限)
实操建议:先给所有想并行的测试加 t.Parallel(),再跑 go test -race 确认无数据竞争;再用 go test -cpu=1,2,4 对比不同并发度下的耗时,找到最优值。
TestMain 怎么用才不污染测试状态
TestMain 是共享初始化的正解,但容易误用成“全局单例污染源”。关键原则是:只共享只读资源,可变状态必须 per-test 隔离。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 好做法:在
TestMain中建立连接池(sql.DB)、加载配置(map[string]string)、启动本地 mock server(绑定随机端口) - ❌ 坏做法:在
TestMain中创建一个全局*sql.Tx或复用http.Client并设置共享 cookie jar - ⚠️ 注意:如果必须初始化可变资源(如临时目录),请用
t.TempDir()替代手动os.MkdirTemp,它会在测试结束自动清理
示例中常漏掉 m.Run() 后的 os.Exit,导致测试退出码异常——务必写全:
func TestMain(m *testing.M) {<br> // 初始化<br> code := m.Run()<br> // 清理(如关闭 db 连接池)<br> os.Exit(code)<br>}
基准测试里 b.ResetTimer() 该在哪儿调
b.ResetTimer() 的作用是“丢弃准备阶段耗时”,不是可有可无的装饰。它必须放在所有一次性 setup 操作之后、循环体之前。否则你会把建连接、加载 fixture 的时间也算进性能指标里。
- ❌ 错误位置:
for i := 0; i —— 每次都新建连接,还重置计时器 - ✅ 正确位置:
func BenchmarkQueryUser(b *testing.B) {<br> db, _ := sql.Open("sqlite3", ":memory:")<br> // 初始化表、插入测试数据<br> setupDB(db)<br> b.ResetTimer() // ← 就在这儿<br> for i := 0; i < b.N; i++ {<br> db.QueryRow("SELECT name FROM users WHERE id = ?", 1)<br> }<br>}
另外,别忘了用 go test -bench=. -benchmem -count=5 | benchstat 多次运行取平均,避免单次抖动误导判断。
db.SetMaxOpenConns(1) 模拟单连接瓶颈;或者 benchmark 里没关 GC,导致 B/op 看着低,实际线上一压就卡。这些细节不查 pprof 和 benchstat 输出,光看 pass/fail 根本发现不了。











