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

sql.Rows.Next 返回 false 后还要检查什么错误?

来源:17golang原创

时间:2026-11-13 10:47:23 403浏览 收藏

使用 Go 的 database/sql 读取多行结果时,rows.Next() 返回 false 不能直接当成“查询正常结束”。它同时表示“没有下一行”或“准备下一行时出现错误”,因此循环结束后必须检查 rows.Err()。最小且可靠的判断顺序是:循环内处理 Next 和 Scan,循环后检查 Err,并保证 Rows 能够关闭。

官方地址:https://pkg.go.dev/database/sql

如果只写了 for rows.Next() { ... },网络中断、上下文取消、驱动读取失败等情况就可能被当成“数据已经读完”,最后返回一份不完整但看起来正常的结果。

Next 返回 false 到底代表什么

Rows.Next 负责把游标推进到下一行。返回 true 时,当前行可以交给 Scan;返回 false 时,既可能是没有下一行,也可能是推进过程中发生了错误。它本身只返回布尔值,不携带错误详情。

现象应该查看的对象可以得出的结论
Next() == true继续调用 Scan当前行已经准备好
Next() == false 且 Err() == nil循环正常结束没有更多可读行
Next() == false 且 Err() != nil返回或包装 Err()读取过程失败,结果不能当成完整结果使用
sql.Rows 的 Next、Scan 与 Err 三个方法之间的关系说明图
图1:结构图展示 Next 返回 true 后才能 Scan,以及 Next 返回 false 后用 Err 区分结束和失败;这不是运行截图。

循环结束后先检查 rows.Err

官方推荐的写法是把 rows.Err() 放在遍历循环之后。这样既不会把每一轮的正常读取和最终的迭代错误混在一起,也能保留驱动或上下文取消提供的具体错误。

func loadNames(ctx context.Context, db *sql.DB) ([]string, error) {
	// 让数据库读取跟随调用方的取消和超时信号。
	rows, err := db.QueryContext(ctx, `SELECT name FROM users ORDER BY id`)
	if err != nil {
		return nil, fmt.Errorf("执行查询失败: %w", err)
	}
	// 无论是正常返回还是中途出错,都要释放结果集资源。
	defer rows.Close()

	var names []string
	for rows.Next() {
		var name string
		// Scan 只负责当前行的列映射,错误要立即停止读取。
		if err := rows.Scan(&name); err != nil {
			return nil, fmt.Errorf("读取当前行失败: %w", err)
		}
		names = append(names, name)
	}

	// Next 为 false 后,用 Err 区分读完和读取失败。
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("遍历结果集失败: %w", err)
	}
	return names, nil
}

这里的关键点是:Scan 错误和 rows.Err 错误处在不同阶段。前者表示当前行无法映射到目标变量,后者表示继续推进结果集时出现了错误。两者都应该让调用方知道本次结果不能按完整数据处理。

为什么还要写 defer rows.Close

遍历到最后一行后,标准库通常会自动关闭结果集;但函数也可能在循环中因为 Scan 错误、业务过滤或上层取消提前返回。入口处立刻注册 defer rows.Close(),可以覆盖这些提前退出路径。

如果使用的场景涉及批量语句、写入型结果或驱动明确提供了关闭错误,还可以把 Close 的返回值单独处理。需要注意的是,Close 不能代替 Err:前者处理资源关闭,后者报告遍历期间记录的错误。

func readWithClose(ctx context.Context, db *sql.DB) (err error) {
	// 查询成功后立刻登记兜底关闭动作。
	rows, err := db.QueryContext(ctx, `SELECT id FROM jobs`)
	if err != nil {
		return fmt.Errorf("查询任务失败: %w", err)
	}
	defer func() {
		// 只有主体没有更早返回错误时,才把 Close 错误作为最终错误。
		if closeErr := rows.Close(); err == nil && closeErr != nil {
			err = fmt.Errorf("关闭结果集失败: %w", closeErr)
		}
	}()

	for rows.Next() {
		var id int64
		// 这里只演示读取阶段,真实业务应继续处理 id。
		if scanErr := rows.Scan(&id); scanErr != nil {
			return fmt.Errorf("读取任务编号失败: %w", scanErr)
		}
	}
	// 先保存遍历错误,避免 Close 的清理逻辑覆盖它。
	if err := rows.Err(); err != nil {
		return fmt.Errorf("遍历任务失败: %w", err)
	}
	return nil
}

上下文取消时不要把部分结果当成功

使用 QueryContext 时,调用方取消上下文或超时,驱动可能在下一次读取时返回错误。此时循环可能只是提前停止,调用方仍然需要通过 rows.Err() 识别失败。不要根据已经追加到切片里的行数判断查询是否完整,也不要因为切片非空就返回成功。

func loadPage(ctx context.Context, db *sql.DB) ([]int64, error) {
	// 让调用方能够在请求结束时停止数据库读取。
	rows, err := db.QueryContext(ctx, `SELECT id FROM events ORDER BY id`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var ids []int64
	for rows.Next() {
		var id int64
		// 当前行可读不等于整个结果集已经完整读完。
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		// 取消或网络读取错误时,丢弃部分结果并把错误交给上层。
		return nil, fmt.Errorf("结果集未完整读取: %w", err)
	}
	return ids, nil
}

多结果集还要检查 NextResultSet

当驱动支持多结果集时,第一组数据读完后通常还要调用 NextResultSet。它同样只返回布尔值:没有下一组结果和切换过程中发生错误,都可能得到 false。因此每次结果集遍历完成后,最终仍要检查 rows.Err()。

func readSets(ctx context.Context, db *sql.DB) error {
	// 多语句是否可用取决于数据库和驱动配置,这里只演示判断顺序。
	rows, err := db.QueryContext(ctx, `SELECT id FROM a; SELECT id FROM b`)
	if err != nil {
		return err
	}
	defer rows.Close()

	for {
		for rows.Next() {
			var id int64
			// 每进入一个结果集,都必须先 Next 再 Scan。
			if err := rows.Scan(&id); err != nil {
				return fmt.Errorf("扫描结果集失败: %w", err)
			}
		}
		// 当前结果集结束后先检查迭代错误。
		if err := rows.Err(); err != nil {
			return fmt.Errorf("读取结果集失败: %w", err)
		}
		// false 可能表示没有下一组,也可能表示切换失败。
		if !rows.NextResultSet() {
			if err := rows.Err(); err != nil {
				return fmt.Errorf("切换结果集失败: %w", err)
			}
			return nil
		}
	}
}
sql.Rows 正常读完、读取失败、关闭和多结果集边界说明图
图2:结构图对比正常读完、读取失败、上下文取消、显式 Close 和多结果集边界;这不是运行截图。

把这条检查规则记成一个固定模板

对于普通的单结果集查询,可以把判断收敛成四句话:查询调用先检查返回错误;循环中每次 Next 为真才调用 Scan;循环结束后检查 rows.Err;函数退出前保证 rows.Close。如果是可取消请求,就使用 QueryContext;如果是多结果集,就在 NextResultSet 返回假之后再次查看 rows.Err。

最容易遗漏的不是 Scan,而是把 Next 返回 false 当作成功结束。只要结果可能受到网络、驱动、上下文或多结果集切换影响,rows.Err() 就是判断这次读取是否完整的最后一道检查。

相关问题

  • 空结果集会让 rows.Err 返回错误吗? 不会。没有任何行但正常结束时,Next 返回 false,Err 通常仍为 nil。
  • QueryRow 没有结果时也检查 Rows.Err 吗? 不需要。QueryRow 返回的是 *sql.Row,应检查 Scan 返回的 sql.ErrNoRows 或其它错误。
  • 只调用 rows.Close 能发现读取错误吗? 不能。关闭负责资源边界,遍历错误要通过 rows.Err 获取。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>