当前位置:首页 > 文章列表 > Golang > Go教程 > database/sql Rows 逐行读取并确保连接归还的实现方式

database/sql Rows 逐行读取并确保连接归还的实现方式

来源:17golang原创 2026-11-12 18:05:56 0浏览 收藏

用 Go 读取数据库查询结果时,database/sql 不会把全部记录一次性变成切片。QueryContext 返回一个 *sql.Rows,调用方需要通过 Next 移动到下一行,再用 Scan 把当前行写入变量。

可靠的写法是:查询成功后立刻安排 Rows.Close,循环里只在 Next 返回 true 时调用 Scan,循环结束再检查 Rows.Err。这样既能处理逐行读取,也能覆盖提前返回和迭代失败,避免结果集长期占用连接池资源。

官方资料:https://pkg.go.dev/database/sql

先分清 DB、Rows 和连接池的职责

*sql.DB 是可长期共享的数据库句柄,内部负责管理连接池;它不是某一条固定连接。执行查询后得到的 *sql.Rows 则代表本次结果集的读取状态,调用方必须负责把它读完或关闭。

因此,代码里最重要的生命周期边界不是“函数最后有没有 return”,而是“拿到 Rows 后是否有一条必达的 Close 路径”。自然读完时 Rows 会自动关闭,但显式安排 Close 仍然能覆盖扫描失败、业务筛选后提前返回等分支。

database/sql 的 DB、Rows 游标、Next Scan 和连接池关系说明图
图1:QueryContext 获取 Rows 后,逐行读取结果并在结束时把资源交还连接池;这是静态结构说明图,不是运行截图。

用 Next 和 Scan 逐行读取

每次调用 Next,Rows 都会准备一行数据;只有返回 true 时,下一步的 Scan 才有当前行可读。Scan 的目标参数数量和类型要能接住查询列,否则应立即返回错误。

package data

import (
	"context"
	"database/sql"
)

type User struct {
	ID   int64
	Name string
}

func readUsers(ctx context.Context, db *sql.DB) ([]User, error) {
	// 使用请求上下文,让取消或超时可以传递给数据库查询。
	rows, err := db.QueryContext(ctx, `SELECT id, name FROM users ORDER BY id`)
	if err != nil {
		return nil, err
	}
	// 任何提前 return 的分支都要关闭结果集,释放它持有的资源。
	defer rows.Close()

	users := make([]User, 0, 16)
	for rows.Next() {
		var user User
		// Scan 只读取 Next 已定位的当前行,失败时停止继续消费。
		if err := rows.Scan(&user.ID, &user.Name); err != nil {
			return nil, err
		}
		users = append(users, user)
	}
	// Next=false 可能是正常读完,也可能是迭代期间发生了错误。
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return users, nil
}

这里的容量预估只是示例,不代表查询结果一定只有 16 行。真正需要记住的是调用顺序:先判断查询错误,再安排关闭;循环里先 Next 后 Scan;循环外再看 Err。

用 Close 覆盖提前退出路径

有些业务并不需要把全部结果读完,例如找到首个匹配项后就返回,或者发现一行数据不符合转换规则时立即终止。此时循环不会自然走到结果集末尾,defer rows.Close() 就是兜底路径。

Close 可以重复调用;自然结束后再执行一次延迟关闭不会因为重复关闭而破坏已经记录的 Rows.Err。如果应用特别关心多语句查询或驱动在关闭阶段报告的错误,可以把关闭错误合并到返回值,而不是无条件覆盖更早发生的扫描错误。

func findFirstUser(ctx context.Context, db *sql.DB, prefix string) (user User, err error) {
	// 查询参数通过占位符传入,避免把输入拼接进 SQL 文本。
	rows, err := db.QueryContext(ctx,
		`SELECT id, name FROM users WHERE name LIKE ? ORDER BY id`, prefix+"%")
	if err != nil {
		return User{}, err
	}
	// 即使找到第一行就返回,也要执行 Close。
	defer func() {
		if closeErr := rows.Close(); err == nil && closeErr != nil {
			err = closeErr
		}
	}()

	if rows.Next() {
		// 只取第一行后提前结束,Close 负责释放未消费完的结果集。
		if scanErr := rows.Scan(&user.ID, &user.Name); scanErr != nil {
			return User{}, scanErr
		}
		return user, nil
	}
	// 没有行不等于一定没有错误,先检查迭代状态。
	if err := rows.Err(); err != nil {
		return User{}, err
	}
	return User{}, sql.ErrNoRows
}
Rows Next Scan Err Close 与连接归还边界的关系图
图2:Rows 的读取结果与 Close、Err 的职责边界;这是静态关系说明图,不是运行结果或截图。

在循环后检查 Err

Next 返回 false 时,可能只是已经没有下一行,也可能是驱动读取网络数据、解码结果或推进结果集时出现错误。只看循环是否结束,会把后者误判成成功。

所以 rows.Err() 必须放在循环之后。它可以在显式或隐式 Close 后调用;如果返回非 nil,就把这个错误交给上层。对于多个结果集,还要根据驱动能力使用 NextResultSet,进入下一个结果集后仍然要先调用 Next 再 Scan。

把生命周期规则收进可复用函数

业务层最好只接收“结果列表 + error”,不要让每个调用方重新编写 Next、Scan、Close 和 Err 的组合。封装函数时,可以把字段映射、最大读取数量和上下文作为明确参数,把资源释放责任留在函数内部。

位置应做什么常见误区
QueryContext 成功后立即安排 Rows.Close等业务判断后再决定是否关闭
读取当前行先 Next,再 Scan没有 Next 就直接 Scan
Scan 返回错误停止读取并返回忽略错误继续追加半成品
循环结束后检查 Rows.Err把 Next=false 一律当成成功
提前返回或只取一行依赖 defer Close 兜底认为 return 会自动归还结果集资源

常见问题

Rows 读完后还需要 Close 吗?

正常读完时 Next 在没有更多结果集时会自动关闭 Rows,但仍建议在查询成功后立即安排显式 Close,以覆盖提前返回和异常分支。

为什么不能只判断 rows.Next()?

因为 false 同时表示“没有下一行”和“准备下一行时发生错误”。循环后调用 rows.Err() 才能区分两种情况。

Rows.Close 会关闭整个数据库连接吗?

它关闭的是当前结果集并释放本次读取占用的资源;*sql.DB 仍然是可复用的连接池句柄,不应把每次读取都理解成关闭数据库。

总结:把 *sql.Rows 当成需要明确管理的读取生命周期:查询成功后安排 Close,循环内用 Next 和 Scan 逐行处理,循环后检查 Err。这套顺序能同时覆盖正常结束、提前退出、扫描失败和驱动迭代错误,让连接池资源按预期归还。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
unsafe.StringData 转换字符串只读字节的边界说明unsafe.StringData 转换字符串只读字节的边界说明
上一篇
unsafe.StringData 转换字符串只读字节的边界说明
reflect.Value.IsZero 判断零值并避免类型断言的方法
下一篇
reflect.Value.IsZero 判断零值并避免类型断言的方法
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    999次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1002次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1122次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1097次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    941次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码