登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go database/sql Rows 没有关闭为什么连接耗尽怎样构造最小复现

来源:17golang原创

时间:2026-11-07 14:15:23 145浏览 收藏

在 Go 的 database/sql 中,查询返回的 *sql.Rows 还没有结束或关闭时,底层连接可能继续处于使用中。如果连接池的 MaxOpenConns 已经到顶,后续查询就会表现为一直等待,看起来像“连接耗尽”。最直接的修复是:拿到 Rows 后立刻安排 defer rows.Close(),循环结束后再检查 rows.Err()。

官方文档:https://pkg.go.dev/database/sql

这不是简单把连接上限调大就能根治的问题。先确认是否有提前返回、错误分支或只读了部分结果却没有关闭 Rows,再用一个只有 1 个连接的测试把现象固定下来。

先看连接为什么像是耗尽

DB 代表的是连接池,不是一条固定连接。QueryContext 返回 Rows 后,结果集的生命周期仍然影响连接何时可以回到池中。完整读到末尾时,Rows 会在迭代结束时关闭;但只要代码在循环中途 return、break,或者在错误分支直接离开函数,就不能把自动结束当成释放策略。

最容易漏掉的写法通常类似这样:

rows, err := db.QueryContext(ctx, query)
if err != nil {
    return err
}
for rows.Next() {
    if err := rows.Scan(&item); err != nil {
        // 扫描失败会提前离开,当前 Rows 仍需要关闭。
        return err
    }
    if shouldStop(item) {
        // 业务只要前几条时,不能依赖循环自然走到末尾。
        return nil
    }
}
return rows.Err()

当池上限很小、请求并发又较高时,这种遗漏会把“结果集没有释放”放大成“后续 Query 等不到连接”。下图只画资源归属关系,帮助定位关闭责任,不是数据库控制台截图或运行结果。

database/sql、连接池、Query、Rows 与 Rows.Close 的静态资源归属结构图
图1:连接池归属说明图,展示 Query 返回 Rows 后资源仍由结果集持有;这是静态结构图,不是运行截图。

用一个连接池构造最小复现

为了不依赖真实数据库,可以在测试中使用 go-sqlmock。把最大打开连接数设为 1:第一条查询拿到 Rows 后故意不关闭,第二条查询就只能等待;一旦关闭第一组 Rows,第二条查询才有机会继续。

package sqlrows_test

import (
    "testing"
    "time"

    "github.com/DATA-DOG/go-sqlmock"
)

func TestRowsMustCloseBeforeNextQuery(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close() // 测试结束时释放连接池。
    db.SetMaxOpenConns(1) // 只有一个连接,便于固定复现条件。

    mock.ExpectQuery(`SELECT id`).WillReturnRows(
        sqlmock.NewRows([]string{"id"}).AddRow(1),
    )
    mock.ExpectQuery(`SELECT id`).WillReturnRows(
        sqlmock.NewRows([]string{"id"}).AddRow(2),
    )

    rows, err := db.Query("SELECT id")
    if err != nil {
        t.Fatal(err)
    }
    // 这里故意不调用 rows.Close,模拟业务提前返回。

    secondDone := make(chan error, 1)
    go func() {
        nextRows, queryErr := db.Query("SELECT id")
        if queryErr == nil {
            queryErr = nextRows.Close() // 第二次查询拿到结果后也要关闭。
        }
        secondDone 

这个测试关注的是连接池等待关系,不是某个数据库驱动的性能结论。真实项目中可以把 sqlmock 替换为当前驱动,但仍然保留 SetMaxOpenConns(1)、第一组 Rows 不关闭、第二次 Query 等待、随后 Close 释放这四个条件。

修复提前返回和循环读取

生产代码的关键顺序是:查询成功后马上安排关闭;每次 Scan 处理错误;循环自然结束后检查 Close 和 Err。如果业务只需要一条记录,优先考虑 QueryRowContext,避免手动管理多行结果集;如果确实使用 Rows,就把关闭责任放在取得它的同一层函数里。

func loadNames(ctx context.Context, db *sql.DB) ([]string, error) {
    rows, err := db.QueryContext(ctx, `SELECT name FROM people WHERE active = ?`, true)
    if err != nil {
        return nil, err
    }
    defer func() {
        // 覆盖 Scan 失败、业务提前返回等所有离开路径。
        _ = rows.Close()
    }()

    var names []string
    for rows.Next() {
        var name string
        if err := rows.Scan(&name); err != nil {
            return nil, err
        }
        names = append(names, name)
    }
    if err := rows.Err(); err != nil {
        // Next 结束不等于读取成功,驱动错误要从 Err 取出。
        return nil, err
    }
    return names, nil
}

若业务需要把 Close 的错误也纳入返回值,可以用命名返回值在 defer 中合并错误;普通查询至少不要吞掉 rows.Err()。图2把读取路径和释放路径分开画出,重点是提前返回也必须经过 Close 边界。

QueryContext、Rows.Next、Rows.Scan、Rows.Close 和 Rows.Err 的静态职责结构图
图2:Rows 读取职责结构图,标出提前返回时必须覆盖的 Close 边界;这是静态说明图,不是执行结果。

排查时应该看哪些信号

先看连接池统计,再回到调用路径找 Rows 的创建点和关闭点。DBStats 中的 InUse、Idle、WaitCount 与 WaitDuration 能帮助判断是否存在等待,但它们本身不能证明一定是 Rows 泄漏。

stats := db.Stats()
// InUse 持续接近 MaxOpenConnections 且 WaitCount 增长时,优先检查未释放资源。
log.Printf("open=%d in_use=%d idle=%d waits=%d wait_duration=%s",
    stats.OpenConnections,
    stats.InUse,
    stats.Idle,
    stats.WaitCount,
    stats.WaitDuration,
)

排查可以按下面的顺序收敛:

现象优先检查修复方向
第二次查询等待前一次 Rows 是否提前返回Query 成功后立即 defer Close
循环结束但仍报错rows.Err() 是否被读取在返回结果前检查 Err
InUse 长时间不降事务、Stmt、Rows 的关闭路径让创建资源的函数负责释放
WaitCount 突然增加并发量、慢查询和连接上限先修复生命周期,再评估池参数

总之,Rows 未关闭导致的连接等待,核心不是“连接数太少”,而是资源生命周期没有覆盖所有返回路径。先用 MaxOpenConns=1 的测试固定问题,再把 Close、Err 和必要的统计记录补齐。

相关问题

读完所有 rows 后还需要显式 Close 吗?

应该保留显式关闭。完整迭代通常会触发自动关闭,但显式 defer rows.Close() 能覆盖提前返回和未来修改后的分支。

把 MaxOpenConns 调大能解决吗?

它只能暂时延后等待,不能修复未关闭的 Rows。只有在确认生命周期正确后,才根据数据库承载能力调整池参数。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>