sql.db必须全局复用,不能按请求new;它是并发安全的连接池管理器,应在main/init中一次性初始化并配置参数,否则导致文件描述符暴涨和too many open files错误。

sql.DB 必须全局复用,不能按请求 new
很多人在初始化数据库连接时,习惯把 sql.Open 放在 handler 里或每次调用前执行,结果导致连接池失控、文件描述符暴涨、甚至触发系统级报错 too many open files。根本原因是:*sql.DB 本身就是一个并发安全的连接池管理器,不是单个连接;它内部维护空闲连接、自动重试、超时控制和健康检查,天生支持 goroutine 并发访问。
正确做法是程序启动时一次性初始化,全程复用:
- 在
main()或 init 阶段调用sql.Open,立即设置SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime - 紧跟着调用
db.Ping()验证连通性,避免服务“假启动”(进程起来但首次查询才报错) - 将
*sql.DB实例注入到所有需要它的 service 层或 repository,不传参、不闭包、不复制
HTTP 客户端连接池参数必须显式配置 transport
Go 的 http.Client 默认使用 http.DefaultTransport,其连接复用能力受限于默认值:比如 MaxIdleConnsPerHost = 2,在高并发调用下游服务时极易成为瓶颈,表现为大量新建 TCP 连接、TIME-WAIT 爆满、响应延迟毛刺。
关键参数要按场景调整:
-
MaxIdleConnsPerHost:建议设为 50–100(非全局最大数),否则单 host 连接不够用 -
MaxConnsPerHost:硬上限,防止突发流量打垮下游,设为 200 左右较稳妥 -
IdleConnTimeout:设为 30–90s,太短导致频繁重连,太长积压无效连接 -
TLSDialContext或DialContext中务必设Timeout(如 1s)和KeepAlive(如 30s)
别用 http.DefaultClient,每个业务客户端应独立配置 transport,避免互相干扰。
连接池参数配错比不配更危险
常见错误配置会放大故障而非缓解:比如把 SetMaxOpenConns 设成 1000,而 MySQL 侧 max_connections=200,结果服务一压测就触发数据库拒绝新连接;或者 SetConnMaxLifetime 设为 0(即永不过期),遇到 RDS Proxy 主动断连后,连接池持续复用已失效连接,后续请求批量返回 i/o timeout 或 invalid connection。
推荐取值组合(以 MySQL 为例):
-
SetMaxOpenConns(16):对应 DB 侧max_connections=20× 0.8,留缓冲空间 -
SetMaxIdleConns(8):约为 MaxOpen 的 1/2,平衡复用率与资源占用 -
SetConnMaxLifetime(240 * time.Second):略小于 MySQLwait_timeout=300,防 stale connection -
SetConnMaxIdleTime(30 * time.Second):Go 1.15+ 新增,主动清理长期空闲连接
事务未提交会导致连接池“静默泄漏”
*sql.Tx 不是连接池的一部分,但它从池中借出一个连接并独占使用。如果忘记 tx.Commit() 或 tx.Rollback(),该连接不会归还给池,也不会被 GC 立即回收——因为 tx 对象可能还在栈上或被闭包捕获,连接就一直卡住。现象是:监控看到 sql_open_connections 持续上涨,sql_idle_connections 趋近于 0,新请求开始排队超时。
防御手段很直接:
- 所有事务逻辑必须用
defer tx.Rollback()开头,再在成功路径上tx.Commit() - 避免在事务中启动 goroutine 或传递
tx到其他 goroutine,容易丢失生命周期控制 - 用静态检查工具(如
errcheck)强制捕获Commit()/Rollback()的 error 返回值
最隐蔽的问题往往不出现在连接池初始化那一刻,而藏在事务结束那一行没写的 Rollback 里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











