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

Go database/sql.Rows.NextResultSet 怎样读取多结果集

来源:17golang原创

时间:2026-10-21 20:16:44 410浏览 收藏

Go 的 database/sql 读取多结果集时,关键不是把几次 Rows.Next 连在一起,而是分清两个层次:Rows.Next 只在当前结果集中向前移动,Rows.NextResultSet 才负责切换到下一个结果集。每次切换成功后,都要重新调用 Rows.Next,然后才能 Scan。

可以把写法记成一句话:先读完当前结果集,再用 NextResultSet 切换;任何返回 false 的地方,都在适当位置检查 Rows.Err。这样才能区分“确实没有更多数据”和“驱动器在迭代或切换时出错”。

先分清 Rows 的两个游标层次

*sql.Rows 是查询结果的迭代器。一个查询可能包含多个结果集,但当前时刻只有一个结果集处于可读取状态。

  • Rows.Next():准备当前结果集的下一行,返回 true 才能调用 Rows.Scan。
  • Rows.Scan(...):把当前行的列复制到目标变量,目标数量要和当前结果集的列数匹配。
  • Rows.NextResultSet():准备下一个结果集,返回 true 表示还有结果集可切换。
  • Rows.Err():返回迭代过程中遇到的错误,用来解释 Next 或 NextResultSet 返回 false 的原因。
database/sql 查询入口、当前结果集、Rows.NextResultSet 与 Rows.Err 的静态关系图
图1:查询入口、当前结果集与切换方法的静态关系说明图,不是运行截图。

按结果集分别扫描列

下面的示例假设具体数据库驱动器允许一次查询返回两个结果集:第一个结果集返回用户,第二个结果集返回该用户的订单。不同驱动器对多语句查询和多结果集的支持并不完全相同,因此示例重点放在 Rows 的读取契约,而不是某个驱动器的连接配置。

package main

import (
    "context"
    "database/sql"
    "fmt"
)

func readMultipleSets(ctx context.Context, db *sql.DB, userID int64) error {
    // 多结果集能力由具体驱动器决定;这里专注展示 Rows 的游标关系。
    const query = `
        SELECT id, name FROM users WHERE id = ?;
        SELECT id, total FROM orders WHERE user_id = ?;
    `

    rows, err := db.QueryContext(ctx, query, userID, userID)
    if err != nil {
        // 查询阶段失败时还没有可迭代的 Rows。
        return fmt.Errorf("查询多结果集失败: %w", err)
    }
    defer rows.Close() // 无论中途哪一步返回,都释放底层资源。

    // 第一组结果集:每次 Next 成功后,Scan 才能读取当前行。
    for rows.Next() {
        var id int64
        var name string
        if err := rows.Scan(&id, &name); err != nil {
            // Scan 错误直接结束当前读取,交给调用方处理。
            return fmt.Errorf("读取用户结果集失败: %w", err)
        }
        fmt.Printf("user=%d name=%s\\n", id, name)
    }
    if err := rows.Err(); err != nil {
        // false 可能是迭代错误,不能只当成正常结束。
        return fmt.Errorf("遍历用户结果集失败: %w", err)
    }

    // 切换成功后,必须重新调用 Next,再按第二组列结构 Scan。
    if !rows.NextResultSet() {
        if err := rows.Err(); err != nil {
            return fmt.Errorf("切换订单结果集失败: %w", err)
        }
        return fmt.Errorf("查询没有返回订单结果集")
    }

    for rows.Next() {
        var orderID int64
        var total float64
        if err := rows.Scan(&orderID, &total); err != nil {
            // 第二组结果集的列结构与第一组不同,目标变量也要随之改变。
            return fmt.Errorf("读取订单结果集失败: %w", err)
        }
        fmt.Printf("order=%d total=%.2f\\n", orderID, total)
    }
    if err := rows.Err(); err != nil {
        // 这里覆盖第二组结果集的迭代错误。
        return fmt.Errorf("遍历订单结果集失败: %w", err)
    }
    return nil
}

这个例子有两个容易写错的地方。第一,切换到第二组之后不能直接 Scan,因为官方约定是每次 NextResultSet 后都要先调用 Next。第二,每组结果集结束时都应该检查 Rows.Err;如果只检查最后一次,很难判断错误发生在第一组读取还是结果集切换阶段。

空结果集不等于切换失败

NextResultSet 返回 true 只说明下一个结果集存在,并不保证其中有行。切换成功后,下一次 Rows.Next 可能立即返回 false,这时要看 Rows.Err:没有错误表示当前结果集为空或已经正常读完,有错误才表示迭代过程中出现了问题。

多结果集 A 和 B、Rows.NextResultSet、Rows.Err 与 Close 的静态边界关系图
图2:结果集 A/B、游标切换、错误读取和资源关闭的静态关系说明图,不是运行截图。
func readUntilEnd(rows *sql.Rows) error {
    // 只演示结果集边界,不假设每个结果集一定包含数据行。
    for {
        for rows.Next() {
            // 不同结果集的列结构可能不同,真实代码应按已知结构分别 Scan。
            var value any
            if err := rows.Scan(&value); err != nil {
                return fmt.Errorf("扫描当前结果集失败: %w", err)
            }
        }
        if err := rows.Err(); err != nil {
            // 当前结果集读完后,先把迭代错误归因到当前边界。
            return fmt.Errorf("读取当前结果集失败: %w", err)
        }
        if !rows.NextResultSet() {
            // false 加 Err 为空,才表示没有更多结果集。
            return nil
        }
    }
}

生产代码通常不会把所有结果集都扫描成 any,因为不同结果集的列数和类型往往不同。更稳妥的方式是为每个已知结果集写一个小的读取函数,或者在切换前根据业务约定固定列结构。上面的循环只用来突出边界判断。

Rows.Err、Close 和驱动器支持怎么配合

场景应调用的接口判断重点
读取当前结果集的下一行Rows.Next → Rows.Scan只有 Next 返回 true 才能 Scan
切换到下一组结果Rows.NextResultSet切换后必须重新 Next
解释 false 的原因Rows.Err区分正常结束、空结果集和迭代错误
结束读取Rows.Close用 defer 保证资源释放;需要时单独处理 Close 错误

不要把“支持 database/sql”理解成“必然支持多结果集”。多结果集通常还受数据库协议、驱动器和连接参数影响。应用启动时应选定明确支持该查询形态的驱动器,并把错误处理写在结果集边界,而不是只在整个函数最后检查一次。

常见追问

调用 NextResultSet 后为什么还要调用 Next?

因为 NextResultSet 只准备下一个结果集,它不负责准备其中的第一行。按照 database/sql 的约定,任何 Scan 前都要先由 Next 成功定位一行。

NextResultSet 返回 false 就一定是没有更多结果集吗?

不一定。它也可能是在切换过程中遇到了错误。调用 Rows.Err 后,如果得到非空错误,就应该按失败处理;只有错误为空时,才可以把它当成正常结束。

结果集为空时要怎么写?

切换成功后让 Rows.Next 返回 false,再检查 Rows.Err。如果错误为空,就表示这一组没有可扫描的行,继续尝试 NextResultSet 即可。

参考资料:https://pkg.go.dev/database/sql#Rows.NextResultSet、https://pkg.go.dev/database/sql#Rows、https://go.dev/doc/database/querying。

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