当前位置:首页 > 文章列表 > Golang > Go问答 > 数据库迭代

数据库迭代

来源:17golang原创 2026-11-11 17:42:28 0浏览 收藏

在 Go 的 database/sql 查询循环里,Rows.Next() 返回 false 不能直接当成“查询成功读完”。它同时覆盖两种情况:结果集确实没有下一行,或者驱动在准备下一行时遇到了错误。循环结束后检查 Rows.Err(),才知道是哪一种。

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

Next 负责推进光标,Err 负责解释推进失败。正确顺序是:成功时 Scan,退出循环后检查 Err,所有出口都保证 Rows 关闭。

为什么 false 需要再问一次 Err

Next 的返回值只有一个布尔量,它适合告诉调用方“现在还能不能 Scan”,却没有空间同时携带错误对象。因此 false 只是停止读取的信号,不是成功状态。

自然读完时,循环会离开,Rows.Err() 通常返回 nil;如果网络中断、驱动读取失败,或者查询上下文在迭代期间被取消,Next 同样会返回 false,但 Err() 会保留对应错误。忽略这一问,调用方可能把只读到半截的数据当成完整结果。

Go database/sql 中 Rows.Next 返回 false 后通过 Rows.Err 区分自然结束和迭代错误的结构说明图
图1:结构说明图,展示 Rows.Next 与 Rows.Err 的分支关系;这不是运行截图。

先写出最小的正确循环

最小模式只有三个关键位置:Next 成功后才能 Scan;循环退出后检查 Err;不再使用结果集时关闭 Rows。把错误检查放在循环内部并不能替代循环后的检查,因为错误可能发生在某一次推进失败的瞬间。

func listNames(ctx context.Context, db *sql.DB) ([]string, error) {
    // 用 QueryContext 让查询和调用方的取消信号绑定。
    rows, err := db.QueryContext(ctx, `SELECT name FROM users ORDER BY id`)
    if err != nil {
        return nil, fmt.Errorf("query users: %w", err)
    }
    defer rows.Close() // 提前返回时也释放结果集占用的资源。

    var names []string
    for rows.Next() {
        var name string
        // 只有 Next 返回 true 时才有当前行可以 Scan。
        if err := rows.Scan(&name); err != nil {
            return nil, fmt.Errorf("scan user name: %w", err)
        }
        names = append(names, name)
    }

    // false 可能是自然 EOF,也可能是迭代期间发生了错误。
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("iterate users: %w", err)
    }
    return names, nil
}

这个模式的重点不是把 Err 当成额外的“保险检查”,而是把完整结果的判定延后到所有行都尝试推进之后。只要 Err 非空,就不应把已收集的切片当作完整成功结果返回。

一次 Scan 成功,也不代表整个结果集成功

Scan 检查的是当前行到目标变量的转换,例如列类型不匹配、空值无法写入非空目标等;Rows.Err 检查的是迭代过程本身。两者属于不同层次,应该保留不同的错误上下文。

for rows.Next() {
    var id int64
    var title string

    // 当前行的转换错误在 Scan 处处理,不要等到 Err 才猜原因。
    if err := rows.Scan(&id, &title); err != nil {
        return fmt.Errorf("scan article id=%d: %w", id, err)
    }
    consume(id, title)
}

// 这里检查的是下一行准备、驱动读取或上下文取消等迭代错误。
if err := rows.Err(); err != nil {
    return fmt.Errorf("read article rows: %w", err)
}

如果业务允许处理部分结果,也要显式设计返回值,例如返回“已处理数量”和错误,而不是悄悄吞掉 Err。这样调用方才能决定记录断点、重试剩余任务,还是把整个批次标记为失败。

把上下文取消放进同一条错误链

长查询或分页读取通常会使用 QueryContext。当请求超时或上游主动取消时,结果集迭代可能提前结束。此时 Next 的 false 仍然只是表象,真正可判断的原因要从 Err 和上下文错误中取得。

func streamUsers(ctx context.Context, db *sql.DB, handle func(User) error) error {
    rows, err := db.QueryContext(ctx, `SELECT id, name FROM users`)
    if err != nil {
        return fmt.Errorf("start user query: %w", err)
    }
    defer rows.Close() // 无论是超时、Scan 失败还是正常结束都关闭 Rows。

    for rows.Next() {
        var u User
        // 业务处理失败也要立即返回,避免继续消费已经失效的上下文。
        if err := rows.Scan(&u.ID, &u.Name); err != nil {
            return fmt.Errorf("scan user: %w", err)
        }
        if err := handle(u); err != nil {
            return fmt.Errorf("handle user %d: %w", u.ID, err)
        }
    }

    if err := rows.Err(); err != nil {
        // 上层可用 errors.Is 判断 context.Canceled 或 DeadlineExceeded。
        return fmt.Errorf("stream users: %w", err)
    }
    return nil
}
Go 数据库结果集从 QueryContext 到 Rows 迭代、上下文取消和 Close 的生命周期结构说明图
图2:生命周期结构说明图,展示 Rows 资源与上下文取消的关系;这不是运行截图。

这里不建议只检查 ctx.Err() 而跳过 rows.Err()。结果集错误可能来自驱动或连接本身;即使当前上下文没有取消,也不能据此推断迭代成功。

哪些结束场景要区别处理

场景Next 返回值应关注的判断
结果集自然读完falserows.Err() == nil,可以返回完整结果
驱动或连接读取失败falserows.Err() != nil,不要当成完整成功
上下文超时或取消false检查 rows.Err(),必要时用 errors.Is 分类
当前行转换失败true 后 Scan 报错在 Scan 处返回并保留列转换上下文
调用方提前退出未必再次调用 Next用 defer rows.Close() 兜底释放资源

如果使用多结果集,还要分别推进 NextResultSet,并在结果集推进完成后继续检查 Err。不要把“没有下一行”和“没有下一个结果集”混成同一种业务状态。

写查询循环时记住这四个动作

  1. Query 失败先返回:查询尚未得到可迭代的 Rows 时,处理函数本身的错误。
  2. Next 成功才 Scan:不要在 Next 之前读取当前行。
  3. 循环后检查 Err:只有 Err() == nil 才能把自然结束当成成功。
  4. 所有出口 Close:提前返回、超时和业务处理失败都应释放结果集。

一句话总结:Next 决定“还能不能继续读”,Scan 负责“当前行能不能转换”,Err 负责“循环为什么停”。把三者分开处理,数据库迭代就不会因为一次被忽略的错误而产生半截成功。

相关问题

没有查到数据时,Rows.Err 会返回 ErrNoRows 吗?

不会把普通的空结果集当成单行查询的 sql.ErrNoRows。对于 Rows,循环可能一次都不进入,随后用 Err 判断迭代过程是否出错;单行查询则通过 Row.Scan 处理 ErrNoRows。

rows.Close 还需要显式调用吗?

即使某些自然结束路径会隐式关闭,也建议保留 defer rows.Close(),因为函数可能在循环中途因 Scan、业务处理或上下文错误提前返回。

可以只返回已经读到的部分数据吗?

可以,但必须把它定义成明确的部分成功协议,并把 Err 一起交给调用方。默认的查询函数更适合在 Err 非空时返回错误,避免下游误以为切片完整。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
预编译语句
下一篇
预编译语句
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    983次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    987次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1105次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1077次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    918次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码