正确做法是复用*sql.DB并预热连接:在TestMain中初始化db、调用db.Ping()、设置连接池为1、每次循环前执行SELECT 1预热,且必须立即调用rows.Close();测试前须用EXPLAIN确认索引使用、校验数据量、排除锁竞争干扰。

database/sql 的 Benchmark 必须复用 *sql.DB
直接在 BenchmarkXxx 函数里每次调用 sql.Open(),测出来的根本不是查询性能,而是 TCP 握手、TLS 协商、MySQL 认证、连接池初始化的总耗时。真实服务中 *sql.DB 是全局长生命周期对象,测试必须模拟这点。
正确做法是:在 TestMain 或包级变量中初始化一次 db,所有 benchmark 共享它:
var db *sql.DB
func TestMain(m *testing.M) {
var err error
db, err = sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if err != nil {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatal(err)
}
os.Exit(m.Run())
}
- 务必调用
db.Ping(),否则首次Query会包含冷启动延迟 - 测试前用
db.SetMaxOpenConns(1)和db.SetMaxIdleConns(1),排除连接池干扰 - 每次循环前加
db.Exec("SELECT 1")预热连接(尤其 PostgreSQL 驱动对首次查询更敏感)
rows.Close() 不是可选项,是硬性要求
漏掉 rows.Close() 会导致底层 network buffer 不释放,testing.B 的内存统计严重失真,且高并发下可能触发连接泄漏——这不是“可能”,是确定会发生。
写法上别依赖 defer 在循环内执行(有额外开销),统一放在 rows, err := db.Query(...) 后立即调用:
立即学习“go语言免费学习笔记(深入)”;
func BenchmarkQuery(b *testing.B) {
for i := 0; i < b.N; i++ {
rows, err := db.Query("SELECT id, name FROM users WHERE id = ?", i%1000)
if err != nil {
b.Fatal(err)
}
defer rows.Close() // ✅ 放在这里,不是函数末尾
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
b.Fatal(err)
}
}
if err := rows.Err(); err != nil { // ✅ 检查迭代错误
b.Fatal(err)
}
}
}
- 必须检查
rows.Err(),否则 SQL 执行中途出错(如 timeout、network reset)会被静默忽略 - 不用
QueryRow就不必Close,但它内部仍占用连接,高并发压测时建议搭配db.SetConnMaxLifetime
Scan 方式对 Benchmark 结果影响极大
用 struct + 反射(如 sqlx.StructScan)解析 100 行数据,比手动 rows.Scan(&id, &name) 慢 3–5 倍。反射开销在 testing.B.N = 100000 时会被指数级放大。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
性能敏感场景只用显式 Scan:
- 字段数固定、类型明确时,优先写死
Scan(&id, &name, &email) - 若必须用 struct,确保字段有
db:"column_name"tag,且 Go 类型与 DB 类型严格匹配(int64对BIGINT,string对VARCHAR) - 避免在循环内
new(Album),提前分配好切片,复用指针
别被 ORM 的便利性骗了——Benchmark 测的是“你写的代码”+“数据库执行计划”+“网络栈”的组合表现,不是单看 Go。
WHERE 条件是否走索引,决定 Benchmark 有没有意义
同一张表、同样代码,WHERE id = ?(主键查询)和 WHERE created_at > ?(无索引时间范围)的 p99 耗时可能差两个数量级。不确认执行计划就跑 Benchmark,等于拿随机数当性能报告。
测试前必须做三件事:
- 用
EXPLAIN SELECT ...确认type是const、range,而不是ALL(全表扫描) - 用
db.QueryRow("SELECT COUNT(*) FROM users")确认数据量级(100 行 vs 100 万行结果完全不同) - 压测期间禁止其他进程访问同库同表,
SHOW STATUS LIKE 'innodb_row_lock_waits'上升说明锁竞争已污染结果
最常被忽略的是:Benchmark 不是测 Go,是测 SQL。连执行计划都没看,就优化 Scan 或连接池,方向就错了。

















