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

Go database/sql 怎么连续读取存储过程返回的多个结果集

来源:17golang原创

时间:2026-11-05 08:54:14 136浏览 收藏

用 Go 调用一个会返回多组结果的查询或存储过程时,关键不是把所有数据塞进同一个 for rows.Next(),而是把 *sql.Rows 看成“当前结果集游标”。先用 Next 和 Scan 读完当前组,再用 NextResultSet 切到下一组;每次返回 false 后,都要结合 Rows.Err 判断究竟是正常结束还是发生了错误。

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

本文只讨论 Rows.NextResultSet、Next 和 Err 的调用顺序,不展开 MySQL、SQL Server 或其他数据库的存储过程语法。能否得到多组结果,仍取决于所使用的 database/sql 驱动。

先分清两个游标层级

Rows.Next 只在当前结果集内向前移动一行。它返回 true 时才能调用 Scan;返回 false 时,可能是当前结果集已经读完,也可能是读取过程中出现错误。

Rows.NextResultSet 负责准备下一组结果集。它返回 true 后,必须再次调用 Rows.Next,不能直接 Scan。下一组可能有行,也可能是空结果集,所以“切换成功”和“读到一行”是两个不同判断。

Go database/sql 中 Rows、Next、Scan 和 NextResultSet 的结果集关系结构图
图1:Rows 在当前结果集内逐行读取,再通过 NextResultSet 准备下一组;这是静态结构说明图,不是运行截图。

最小读取骨架应该怎么写

如果不同结果集的列数和列类型可能不同,最稳妥的通用入口是先读取当前组的列名,再按列数创建 Scan 目标。下面的示例把每个单元格转换成字符串,仅用于演示结果集边界;业务代码应根据已知列结构扫描到具体类型。

package multiset

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

// ReadResultSets 按结果集边界收集数据,不依赖具体存储过程语法。
func ReadResultSets(ctx context.Context, db *sql.DB, query string, args ...any) ([][][]string, error) {
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("查询多结果集失败: %w", err)
	}
	defer rows.Close() // 无论中途哪一步失败,都把 Rows 交还给驱动。

	var resultSets [][][]string
	for {
		columns, err := rows.Columns()
		if err != nil {
			return nil, fmt.Errorf("读取当前结果集列信息失败: %w", err)
		}

		var currentSet [][]string
		for rows.Next() {
			values := make([]any, len(columns))
			dest := make([]any, len(columns))
			for i := range values {
				// Scan 需要接收每一列地址,列数必须与当前结果集一致。
				dest[i] = &values[i]
			}
			if err := rows.Scan(dest...); err != nil {
				return nil, fmt.Errorf("扫描第 %d 组结果失败: %w", len(resultSets)+1, err)
			}

			row := make([]string, len(values))
			for i, value := range values {
				// 这里只为统一返回类型;金额、时间等字段应使用业务类型。
				row[i] = fmt.Sprint(value)
			}
			currentSet = append(currentSet, row)
		}

		if err := rows.Err(); err != nil {
			// Next 返回 false 后先检查行迭代错误,再尝试切换结果集。
			return nil, fmt.Errorf("读取第 %d 组结果失败: %w", len(resultSets)+1, err)
		}
		resultSets = append(resultSets, currentSet)

		if !rows.NextResultSet() {
			// NextResultSet 的 false 既可能表示没有下一组,也可能表示推进失败。
			if err := rows.Err(); err != nil {
				return nil, fmt.Errorf("切换到下一组结果失败: %w", err)
			}
			break
		}
		// 切换成功后回到循环顶部,下一次 Next 才会读取新结果集的第一行。
	}

	return resultSets, nil
}

这个骨架有三个容易被忽略的点:Columns 和 Scan 都针对当前结果集;当前组结束后先查一次 Err;NextResultSet 返回 false 后还要再查一次 Err。

为什么不能把两个 Next 混成一个循环

单结果集代码通常只有一层循环:

for rows.Next() {
	var id int64
	var name string
	// 中文注释:当前结果集每成功移动一行,才能执行 Scan。
	if err := rows.Scan(&id, &name); err != nil {
		return err
	}
}
if err := rows.Err(); err != nil {
	return err
}

多结果集必须再加一层“结果集循环”。如果只调用一次 Next 循环,第一组读完后游标不会自动把第二组的列结构交给你的扫描代码;如果在 NextResultSet 后直接 Scan,则违反了 API 要求,正确顺序始终是“切换结果集,再 Next,再 Scan”。

空结果集和列结构变化怎么处理

某个结果集可以没有数据行。此时它的 for rows.Next() 一次也不进入,但只要 Rows.Err() 为 nil,就仍应继续调用 NextResultSet。不要用“当前组读取到的行数为 0”当作整个查询结束条件。

不同结果集的列数也可能不同。例如第一组返回用户编号和名称,第二组只返回汇总数量。若代码始终写成 rows.Scan(&id, &name),切换到第二组后就会因参数数量不匹配而失败。动态示例使用当前组的 Columns 解决了列数问题;业务代码则应根据结果集约定,在切换边界选择对应的结构体或字段集合。

Go database/sql 中结果集 A、NextResultSet、结果集 B 与 Err 错误分支的边界关系图
图2:逐行读取与结果集切换属于不同边界,Err 同时承担行迭代和切换失败的判别;这是静态结构说明图,不是运行截图。

Rows.Err 应该放在哪些位置

建议保留两个检查点:

  • 当前结果集的 Next 循环结束后检查一次,定位行迭代或驱动读取错误。
  • NextResultSet 返回 false 后再检查一次,区分“没有更多结果集”和“切换下一组失败”。

Rows.Close 可以用 defer 兜底。官方文档说明,当 Next 已返回 false 且没有后续结果集时,Rows 通常会自动关闭,但显式 defer rows.Close() 更能覆盖提前返回、扫描失败和驱动差异。

数据量大时不要无条件全部缓存

上面的通用函数返回 [][][]string,适合展示调用顺序,却会把所有结果集和所有行保存在内存中。生产代码如果只需要逐组写入文件、转换消息或汇总指标,可以在当前结果集的 Scan 成功后立即处理这一行,只保留当前组需要的状态;只有确实要在内存中统一返回时,才收集完整切片。

无论采用流式处理还是切片收集,都不要为了“快速拿到下一组”而跳过当前组的剩余行。先把当前组读完,再调用 NextResultSet,更容易保持连接和驱动状态的一致性。

驱动能力和调用边界

database/sql 定义了读取多个结果集的接口,但具体驱动是否支持、如何把存储过程结果暴露为多个结果集,仍由驱动实现决定。若 NextResultSet 很快返回 false,先查 Rows.Err;没有错误时,再检查驱动文档和查询本身是否确实返回了多组结果。

还要避免把“多结果集读取”误当成事务控制。是否需要事务、查询是否有副作用、超时和取消如何设置,属于调用方的业务决策;本文的核心只在 Rows 游标的两个层次。

一张表记住调用顺序

调用作用返回后要做什么
Next移动到当前结果集的下一行true 后才能 Scan,false 后查 Err
Scan把当前行列值写入目标变量目标数量必须匹配当前结果集列数
NextResultSet准备下一组结果集true 后必须重新调用 Next
Err报告行迭代或结果集推进错误在两个 false 边界都检查
Close结束 Rows 生命周期通常用 defer 兜底

相关问题

NextResultSet 返回 false 就一定是读完了吗?

不一定。它也可能表示切换时发生错误,必须紧接着调用 Rows.Err 区分两种情况。

切换结果集后可以直接 Scan 吗?

不可以。切换成功后必须先调用 Rows.Next,只有它返回 true 才能扫描当前行。

某组结果没有行时要不要跳过 NextResultSet?

不要。空结果集仍然是一个结果集边界,应在确认 Err 没有错误后继续尝试切换。

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