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

Go database/sql 怎样依次读取多个 SQL 结果集

来源:17golang原创

时间:2026-10-13 01:23:37 456浏览 收藏

Go 的 database/sql 读取多个 SQL 结果集,关键是把“行的移动”和“结果集的切换”分开:当前结果集内部反复调用 rows.Next(),每次成功后再 rows.Scan();当前结果集读完后,再调用 rows.NextResultSet() 进入下一组。切换成功后,仍然必须先调用一次 Next(),不能直接 Scan()。

最稳妥的顺序是:外层负责结果集,内层负责行;内层结束后检查当前行迭代状态,外层结束后再检查是否有切换错误。某一组没有行不等于没有下一组结果集。
要点速览
  • Next 只负责把游标移动到当前结果集的下一行。
  • NextResultSet 只负责准备下一组结果集,返回 false 时要结合 Rows.Err 判断原因。
  • 多结果集通常有不同列结构,不能为了复用变量而强行用同一组 Scan 参数。
  • 查询得到的 Rows 应及时关闭;迭代结束后仍要检查错误。
Go database/sql 中外层结果集游标和内层行游标的关系说明图
图1:外层用 NextResultSet 切换结果集、内层用 Next 读取行的结构说明图,不是运行截图。

先分清 Next 和 NextResultSet 各自移动什么

*sql.Rows 可以看成一组按顺序到达的结果集。每个结果集内部又有自己的行游标。刚拿到 Rows 时,游标在第一组结果集的第一行之前,所以第一次读取也要先调用 Next。

方法负责的层级返回 false 时要关注什么
Next()当前结果集中的下一行可能是当前组读完,也可能发生迭代错误,之后检查 Err()
Scan(...)读取已经由 Next 准备好的当前行目标变量数量和类型要匹配当前组的列
NextResultSet()下一组结果集可能没有下一组,也可能切换时发生错误,之后检查 Err()

官方文档还特别强调:调用 NextResultSet 后,扫描下一组前必须再次调用 Next;下一组结果集也可能完全没有行。因此,“上一组的 Next 返回 false”不能直接结束整个 Rows 的处理。

用两层循环依次读取全部结果集

如果各结果集的列结构相同,可以把每行先读成通用的 []any;如果列结构不同,则应在外层记录结果集编号,并为不同编号选择不同的 Scan 目标。下面的骨架只演示游标顺序,不绑定某个数据库的存储过程写法。

package main

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

// readAllResultSets 展示先读行、再切换结果集的固定顺序。
func readAllResultSets(ctx context.Context, db *sql.DB, query string) error {
    rows, err := db.QueryContext(ctx, query)
    if err != nil {
        return fmt.Errorf("query multiple result sets: %w", err)
    }
    defer rows.Close() // 无论中途哪一步失败,都释放 Rows 关联的资源。

    resultSetIndex := 0
    for {
        // 当前结果集可能为空;只有 Next 为 true 时才允许 Scan。
        for rows.Next() {
            var first, second any
            if err := rows.Scan(&first, &second); err != nil {
                return fmt.Errorf("scan result set %d: %w", resultSetIndex, err)
            }
            fmt.Printf("set=%d first=%v second=%v\n", resultSetIndex, first, second)
        }
        // Next 为 false 既可能是正常读完,也可能是迭代错误。
        if err := rows.Err(); err != nil {
            return fmt.Errorf("iterate result set %d: %w", resultSetIndex, err)
        }

        // 当前组读完后,才尝试准备下一组;返回 false 代表整个序列结束或切换失败。
        if !rows.NextResultSet() {
            if err := rows.Err(); err != nil {
                return fmt.Errorf("advance after result set %d: %w", resultSetIndex, err)
            }
            break
        }
        resultSetIndex++
    }
    return nil
}

这段代码的两个判断不能合并成“Next 返回 false 就退出”:内层循环结束只说明当前结果集暂时没有下一行,外层还要给 NextResultSet 一次机会。反过来,NextResultSet 返回 false 后,也不能直接把它当作成功结束,必须先看 Rows.Err()。

空结果集和不同列结构怎么处理

一个结果集可以没有任何行,但后面仍然存在下一组结果集。此时内层 for rows.Next() 一次都不进入,随后只要 rows.Err() 没有错误,仍然应该继续调用 NextResultSet()。

更常见的边界是:第一组返回用户列,第二组返回统计列,两组列数和类型不同。这时可以按结果集编号分支,但仍保持相同的游标顺序:

// processCurrentSet 按结果集编号选择 Scan 目标,避免列结构错配。
func processCurrentSet(rows *sql.Rows, setIndex int) error {
    for rows.Next() {
        switch setIndex {
        case 0:
            var id int64
            var name string
            if err := rows.Scan(&id, &name); err != nil {
                return fmt.Errorf("scan user set: %w", err)
            }
            fmt.Printf("user id=%d name=%s\n", id, name)
        case 1:
            var total int64
            if err := rows.Scan(&total); err != nil {
                return fmt.Errorf("scan summary set: %w", err)
            }
            fmt.Printf("total=%d\n", total)
        default:
            return fmt.Errorf("unexpected result set index %d", setIndex)
        }
    }
    // 当前结果集为空时,这里仍然返回 nil,是否还有下一组由调用方决定。
    return rows.Err()
}

如果结果集数量不是固定的,不能提前假设每一组都有两列。可以先读取 rows.Columns() 或 rows.ColumnTypes() 决定解析策略,但这些方法仍然只描述当前结果集;切换后需要重新读取列信息。

Go 多结果集读取中空结果集、Scan错误和NextResultSet错误的处理边界说明图
图2:空结果集、扫描失败和切换失败的处理边界说明图,不是运行截图。

循环结束后检查 Err 和 Close

Rows.Err() 负责报告遍历过程中遇到的错误,也可以在显式或隐式 Close 后调用。实务中建议同时保留 defer rows.Close() 和关键位置的 Err 检查:前者兜底释放资源,后者让错误尽早带着结果集编号返回。

如果主动在中途停止消费,例如只需要第一组结果集,应明确调用 rows.Close(),不要继续调用 NextResultSet。如果完整消费到最后一组,驱动可能在没有更多结果集时自动关闭,但显式关闭仍然能让生命周期更清楚。

排查顺序
  1. 确认驱动本身支持多结果集,并确认查询确实返回了多组。
  2. 确认每次 Scan 前都有一次成功的 Next。
  3. 确认当前组结束后调用了 NextResultSet,而不是重新执行查询。
  4. 分别在当前组结束和切换失败处检查 Rows.Err()。
  5. 确认不同结果集的列结构没有复用错误的 Scan 参数。

常见问题

NextResultSet 返回 false 就一定是没有下一组吗?

不一定。它既可能表示没有更多结果集,也可能表示切换过程中发生错误。应立即检查 rows.Err() 来区分。

空结果集需要单独调用什么方法吗?

不需要。内层 Next 直接返回 false 后,先检查 Err,再继续调用 NextResultSet 即可。

能不能在 NextResultSet 后直接 Scan?

不能。切换到下一组后,必须先调用 Next,因为游标仍然位于该组第一行之前。

官方 API 文档在哪里?

可直接查看 https://pkg.go.dev/database/sql#Rows.NextResultSet,重点关注 Next、NextResultSet、Err 和 Close 四个方法的关系。

小结:把 Rows 想成“结果集容器中的行游标”即可:内层 Next 处理行,外层 NextResultSet 处理组,Scan 永远跟在 Next 后面,错误统一通过 Err 收口。这样即使遇到空组、异构列或驱动切换错误,代码的控制流也不会混乱。

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