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的原因。

按结果集分别扫描列
下面的示例假设具体数据库驱动器允许一次查询返回两个结果集:第一个结果集返回用户,第二个结果集返回该用户的订单。不同驱动器对多语句查询和多结果集的支持并不完全相同,因此示例重点放在 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:没有错误表示当前结果集为空或已经正常读完,有错误才表示迭代过程中出现了问题。

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。
-
233 收藏
-
173 收藏
-
385 收藏
-
269 收藏
-
135 收藏
-
265 收藏
-
139 收藏
-
169 收藏
-
Golang · Go教程 | 1天前 | 并发 · Go教程 · trace.NewTask go tool trace Go runtime/trace 执行跟踪 trace.WithRegion391 收藏
-
184 收藏
-
Golang · Go教程 | 1天前 | 错误处理 · 模板 · Go教程 · html/template · 模板渲染 Go html/template missingkey 模板缺失字段 Execute错误371 收藏
-
326 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习