避免循环中执行db.Query或db.Exec,应合并查询或分批处理;合理设置连接池参数;优先使用QueryRow查单行并检查错误;明确指定SELECT字段并建立覆盖索引。

避免在循环里执行 db.Query 或 db.Exec
这是最常见也最容易被忽视的性能杀手。每次调用 db.Query 都会建立新连接(或从连接池取一个)、解析 SQL、传输数据、返回结果,开销远高于单次批量操作。
典型错误写法:
for _, id := range ids {
var name string
db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
// 每次都发起一次 round-trip
}正确做法是合并查询或使用批量操作:
- 用
IN语句一次性查多个:SELECT name FROM users WHERE id IN (?, ?, ?)(注意参数数量限制,MySQL 默认 65535) - 对超大列表,分批处理(如每 500 个一组)
- 若逻辑必须逐条处理,考虑先用一次查询拉全量数据到内存,再本地遍历匹配
合理设置 sql.DB 连接池参数
Go 的 sql.DB 本身不是连接,而是连接池管理器。默认配置(MaxOpenConns=0、MaxIdleConns=2)在高并发下极易成为瓶颈。
立即学习“go语言免费学习笔记(深入)”;
关键参数与建议值(以 MySQL 为例):
-
db.SetMaxOpenConns(50):控制最大并发连接数,设为 0 表示无限制(危险!);应略高于峰值 QPS × 平均查询耗时(秒) -
db.SetMaxIdleConns(20):空闲连接上限,过小会导致频繁建连/断连;一般设为MaxOpenConns的 1/2~2/3 -
db.SetConnMaxLifetime(30 * time.Minute):避免连接因数据库侧超时被静默中断;建议显式设置(MySQL 默认 wait_timeout=28800 秒)
漏设 SetConnMaxLifetime 常导致偶发 invalid connection 错误,且难以复现。
用 QueryRow 替代 Query 查单行,且务必检查 Err
看似微小的差异,实际影响执行路径和资源释放:
-
Query返回*Rows,即使只读一行也需手动rows.Close(),否则连接不会归还池中,久而久之耗尽连接 -
QueryRow内部自动关闭,语义更清晰,性能略优 - 但很多人忽略
QueryRow.Scan()的返回值:err可能是sql.ErrNoRows(正常),也可能是网络错误或类型不匹配——不检查就直接用变量,会埋下静默 bug
正确模式:
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 123).Scan(&name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 处理不存在
} else {
// 处理真实错误
}
}慎用 SELECT *,明确指定字段并加索引
不只是“减少网络传输”这么简单。它直接影响查询计划、内存占用和 GC 压力:
- 数据库需读取并传输更多页,IO 和带宽压力上升
- Go 层需分配更大结构体或 map,尤其当字段含
TEXT/BLOB时,可能触发堆分配和后续 GC - 若表有 20 列但只用其中 2 列,却没给这 2 列建联合索引,数据库大概率走全表扫描
实操建议:
- 永远用
SELECT col1, col2替代SELECT * - 对高频
WHERE + ORDER BY组合,建覆盖索引(例如WHERE status=? ORDER BY created_at DESC→ 索引(status, created_at)) - 用
EXPLAIN确认是否走了索引;Go 中可临时开启 MySQL 的slow_query_log抓未命中索引的慢查询
索引不是越多越好,写多读少的表要权衡维护成本;但读多写少的业务表,字段级索引往往是性价比最高的优化点。











