database/sql 连接池不能在 init 或 create 函数中 defer db.close(),否则连接池提前关闭导致后续查询报“sql: database is closed”;正确做法是全局复用 *sql.db 实例,并在程序退出前(如 main 结尾或信号处理)调用 db.close()。

database/sql 连接池不能在 init 或 Create 函数里 defer db.Close()
连接会提前关闭,后续所有查询都报 sql: database is closed。这是因为 defer 在函数返回时立即执行,而 Create() 返回的是 *sql.DB 指针——defer db.Close() 一执行,连接池就销毁了,main 里拿到的只是个空壳。
正确做法是把 db.Close() 放到程序退出前,比如 main() 结尾或 os.Interrupt 信号处理里:
- 全局只创建一次
*sql.DB实例,复用连接池,不要在 handler 里反复调用sql.Open() - 如果用依赖注入(如 wire、fx),确保
db是 singleton,且Close()绑定到应用 shutdown 阶段 - 测试中若需隔离,用
db.Close()+ 新建,但生产环境绝不应在初始化函数中 defer
必须显式配置 SetMaxOpenConns、SetMaxIdleConns 和 SetConnMaxLifetime
默认值对生产环境极不友好:MaxOpenConns=0(无上限)、MaxIdleConns=2、ConnMaxLifetime=0(永不过期)。这会导致数据库连接被打爆、连接泄漏、或卡在已下线节点上。
推荐配置(以 QPS 中等、PostgreSQL 为例):
-
db.SetMaxOpenConns(50):根据 DB 最大连接数设上限,别超过 PostgreSQL 的max_connections -
db.SetMaxIdleConns(25):设为MaxOpenConns的 1/2~2/3,平衡复用与资源占用 -
db.SetConnMaxLifetime(30 * time.Minute):强制连接轮换,避免云数据库节点切换后连接卡死 -
db.SetConnMaxIdleTime(5 * time.Minute)(Go 1.15+):适配 SLB 或 Kubernetes Service 的空闲断连策略
单次查询必须用 QueryContext / ExecContext,不能只靠 db.SetConnMaxLifetime
context.WithTimeout 不影响连接池生命周期,但它能防止 goroutine 卡死在“等连接”或“等查询响应”上。连接池空闲耗尽时,db.Query() 会无限阻塞;没传 context 的 db.Ping() 也可能 hang 住整个健康检查。
关键点:
- 所有外部可触发的操作(HTTP handler、定时任务、RPC 方法)都应带 context,并设合理超时,例如
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - 用
db.QueryContext(ctx, ...)、tx.StmtContext(ctx, ...)、db.PingContext(ctx) - 错误判断要区分:
errors.Is(err, context.DeadlineExceeded)表示等连接或执行超时;errors.Is(err, sql.ErrNoRows)才是业务逻辑无数据
连接池状态得靠 db.Stats() 监控,不能只靠静态配置
配置再合理,也得看运行时是否真有效。光看 QPS 和延迟不够,得盯连接池内部指标。
每 30 秒调一次 db.Stats(),重点关注:
-
WaitCount和WaitDuration持续上升 → 连接不够,要调高MaxOpenConns -
MaxIdleClosed高 → 空闲连接被频繁回收,可能是MaxIdleConns设太高,或请求毛刺大、连接利用率低 -
OpenConnections接近MaxOpenConns且波动剧烈 → 存在连接未正确释放(比如rows没Close()、事务没Commit()/Rollback()) - 把这些指标打点到 Prometheus,设告警:当
WaitDuration > 100ms持续 1 分钟,就该介入
连接池不是设完参数就一劳永逸的事,它活在运行时里,而 db.Stats() 是你唯一能直接对话它的窗口。漏掉这个,等于闭眼开车。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











