数据库连接断开本身不会直接触发 panic,但 driver.ErrBadConn、"invalid connection" 或空指针解引用等错误未被正确处理时会层层传播最终导致 panic。

数据库连接断开本身不会直接触发 panic,但 driver.ErrBadConn、"invalid connection" 或空指针解引用等错误,在未被正确处理时,会层层传播最终导致 panic —— 尤其当业务代码假设 *sql.DB 永远可用,却忽略了连接池中连接可能已失效的事实。
为什么 db.Query 失败后容易 panic
Go 的 *sql.DB 不是“活连接”,而是连接池句柄;它不自动感知服务端主动关闭(如 MySQL 的 wait_timeout 到期)。一次 db.PingContext 成功,只说明池里至少有一个连接此刻能通,不代表下一次 db.Query 一定能成功。
- 常见 panic 场景:对
rows := db.QueryRow(...)的返回值不做err != nil判断,直接调用rows.Scan()→ 如果rows是 nil(因 QueryRow 内部初始化失败),就会 panic:panic: runtime error: invalid memory address or nil pointer dereference - 另一个坑:用
:=初始化db变量时发生遮蔽(比如在initDB()函数内写db, err := sql.Open(...)),导致包级变量db仍为nil,后续所有调用都 panic -
db.Query返回的err若是driver.ErrBadConn,*sql.DB会自动标记该连接为坏,并在下次复用时尝试新建;但这个过程不透明,业务层若没检查 err,就可能把nil的*sql.Rows传给下游
重连不是靠“捕获 panic 后再 open”,而是靠前置防御 + 幂等重试
panic 是结果,不是原因。指望 recover 后再 sql.Open 重建 *sql.DB 是危险的:全局 db 被覆盖会导致正在运行的 goroutine 使用已关闭句柄,引发竞态或二次 panic。
- ✅ 正确做法:把重连逻辑下沉到单次操作层面,用 context 控制超时,对幂等操作(SELECT/GET)做有限重试
- ❌ 错误做法:在 defer recover 里重新
sql.Open并赋值给全局db;或在 handler 中检测 panic 后 reload 整个 DB 实例 - 重试必须带退避:
100ms → 200ms → 400ms,总耗时建议 ≤ 2 秒,避免雪崩 - 只重试明确可恢复的错误:
driver.ErrBadConn、net.OpError(含"timeout"、"connection refused")、"i/o timeout";其他错误(如语法错、约束冲突)重试无意义
SetConnMaxLifetime 是防断连的第一道防线
MySQL 默认 wait_timeout = 300 秒(5 分钟),而 Go 连接池默认永不过期(SetConnMaxLifetime(0))。这意味着连接可能在服务端已被 kill,客户端还继续往里塞请求,首次 Query 就失败。
立即学习“go语言免费学习笔记(深入)”;
- 必须显式设置:
db.SetConnMaxLifetime(240 * time.Second),比服务端 timeout 小至少 60 秒,留出探测和清理缓冲 - 该设置对所有连接生效,无论 idle 还是 in-use;PostgreSQL 用户还需额外配
tcp_keepalives_idle - 别依赖
db.SetMaxIdleConns或db.SetMaxOpenConns来“缓解”断连——它们管的是数量,不是连接健康度
真正要写的不是“重连代码”,而是健壮的 Query 封装
把错误检查、重试、context 超时全部收进一个函数里,业务层只管调用,不碰底层细节。
func QueryRowWithRetry(ctx context.Context, db *sql.DB, query string, args ...interface{}) (*sql.Row, error) {
var row *sql.Row
var err error
for i := 0; i < 3; i++ {
row = db.QueryRowContext(ctx, query, args...)
err = row.Err()
if err == nil {
return row, nil
}
if !isRetryable(err) {
return nil, err
}
if i == 2 {
break
}
time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond) // 100ms, 200ms, 400ms
}
return row, err
}
<p>func isRetryable(err error) bool {
if errors.Is(err, driver.ErrBadConn) {
return true
}
var opErr *net.OpError
if errors.As(err, &opErr) && (opErr.Timeout() || strings.Contains(opErr.Error(), "refused")) {
return true
}
return false
}
注意:这个封装只适用于 SELECT;INSERT/UPDATE/DELETE 必须由业务层保证幂等(例如带 idempotency_key 字段或先查后判),不能无脑重试。
最易被忽略的一点:**db.PingContext 不是健康检查,只是连接池“有口气”的快照;真正的健康验证必须落在每次 Query 的 err 检查上,且必须立刻处理,不能延迟到 Scan 阶段。**


















