应避免循环中执行db.Query/db.Exec,优先批量操作;及时Close sql.Rows;高频单行查询预编译语句;Scan地址与列类型严格匹配;合理设置连接池参数并配合生命周期控制。

避免在循环里执行 db.Query 或 db.Exec
这是最常见也最致命的性能陷阱:每次调用都建立新连接、解析 SQL、传输数据、等待返回。哪怕用连接池,来回往返和上下文切换开销也极大。
- 批量操作优先用
INSERT INTO ... VALUES (...), (...), (...)而非 N 次单条插入 - 查询多条记录时,用
IN子句代替多次SELECT ... WHERE id = ?(注意IN参数数量限制,PostgreSQL 默认 65535,MySQL 受max_allowed_packet影响) - 实在需要逐条处理,先用一次查询取出所有 ID 或主键,再内存中分组/映射,最后批量查关联数据
正确使用 sql.Rows 并及时调用 rows.Close()
sql.Rows 不是普通结构体,它背后持有数据库连接资源。如果忘记 Close(),连接不会归还给连接池,轻则连接耗尽报 dial tcp: lookup xxx: no such host 或 connection refused,重则服务假死。
- 必须用
defer rows.Close(),且放在rows, err := db.Query(...)之后立即写,不能等到循环结束或条件分支后 - 即使
rows.Next()一次都没进(比如无结果),也要Close();否则连接泄漏 - 用
for rows.Next()遍历时,不要在循环内重复rows.Close(),也不要在rows.Err() != nil后跳过Close()
慎用 database/sql 的 QueryRow 和 Scan 组合
QueryRow 看似简洁,但底层仍会分配 sql.Rows 对象并隐式调用 Close() —— 这个行为不可控,且无法复用预编译语句(*sql.Stmt)。
- 高频单行查询(如配置加载、用户登录校验)应预编译:
stmt, _ := db.Prepare("SELECT name FROM users WHERE id = ?"),然后反复stmt.QueryRow(id).Scan(&name) -
Scan传入的变量地址必须与列类型严格匹配,例如数据库是INT8(int64),就不能传*int,否则 panic 报sql: Scan error on column index 0 - 字段数不匹配、NULL 值未用
sql.NullString等类型接收,都会导致Scan失败,错误被吞掉(只在rows.Err()或row.Err()中体现)
连接池参数不是越大越好:SetMaxOpenConns 和 SetMaxIdleConns
盲目调高 MaxOpenConns 会让数据库瞬间收到大量并发请求,可能触发连接数上限、CPU 打满、锁等待飙升。而 MaxIdleConns 过小会导致频繁建连/断连,过大则浪费内存且延迟回收空闲连接。
立即学习“go语言免费学习笔记(深入)”;
- 建议初始值:
MaxOpenConns = 2 * CPU 核数,MaxIdleConns = MaxOpenConns / 2(例如 8 核机器设为 16 和 8) - 务必配合
SetConnMaxLifetime(如 30m)和SetConnMaxIdleTime(如 5m),避免 DNS 变更、DB 重启后连接僵死 - 上线前用
ab或hey压测,观察pg_stat_activity(PostgreSQL)或SHOW PROCESSLIST(MySQL)中的连接状态分布
db.SetMaxOpenConns(16) db.SetMaxIdleConns(8) db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute)
连接池调优没有银弹,得看 DB 实例规格、SQL 复杂度、平均响应时间。很多人调完参数就以为万事大吉,其实慢查询本身没动,池子再大也只是把瓶颈从连接耗尽转移到了数据库 CPU 或磁盘 IO 上。











