当前位置:首页 > 文章列表 > Golang > Go教程 > Go database/sql 单行查询的 QueryRow 使用方式

Go database/sql 单行查询的 QueryRow 使用方式

来源:17golang原创 2026-10-15 08:22:55 0浏览 收藏

在用户详情、配置读取、按主键查找这类场景里,SQL 通常只期待一条记录。Go 的 database/sql 可以用 QueryRowContext 表达这个意图:查询入口返回一个 *sql.Row,真正的查询错误和“没有记录”会在 Scan 时交付。

最容易写错的地方有三个:把 QueryRow 的返回值误当成查询错误、没有单独处理 sql.ErrNoRows,以及因为照搬 Query 的写法而寻找不存在的 Rows.Close。下面用一个按编号查用户的例子把这三个边界拆开。

官方资料:https://go.dev/doc/tutorial/database-access

先判断 QueryRow 是否适合当前查询

QueryRow 适合“最多需要一条记录”的查询,例如按唯一键取用户、按配置名取一项设置。它不会像 Query 那样返回可遍历的 *sql.Rows;如果结果有多行,Scan 只读取第一行,其余结果由 database/sql 处理掉。

相反,如果业务需要遍历订单、统计多条日志或判断多行结果,就应该使用 QueryContext,配合 defer rows.Close() 和 rows.Err()。不要因为查询语句写的是 SELECT,就默认所有读取都用同一种 API。

需求入口重点错误边界
最多一条记录QueryRowContextScan 读取错误,单独处理 sql.ErrNoRows
需要遍历多条记录QueryContextrows.Close()、rows.Scan() 和 rows.Err()
反复执行同一 SQLPrepareContext + Stmt.QueryRowContext语句创建失败与语句关闭错误

QueryRow 为什么把错误留到 Scan

QueryRowContext 的签名返回的是 *sql.Row,没有第二个 error。这不是它忽略了错误,而是把错误延迟到 Row.Scan:这样调用方可以把“查到一行并转换字段”“没有行”“字段无法转换”放在同一处判断。

业务查询函数、QueryRowContext、SQL 参数、sql.Row、Scan 和 sql.ErrNoRows 的静态关系说明图
图1:QueryRowContext、sql.Row 与 Scan 的静态关系示意图,不是运行截图或执行证据。

因此,下面这种写法不对:

row, err := db.QueryRowContext(ctx, query, id)
// QueryRowContext 只返回 *sql.Row,错误不会在这里以第二个返回值交付。
if err != nil {
    return err
}

正确的关注点是 Scan 返回值。对于上下文取消、驱动拒绝查询、占位参数不匹配等问题,也要从这个错误返回值向上处理。

用 QueryRowContext 写一个可复用的单行查询函数

把数据库读取封装成小函数后,调用方不必了解列顺序,也不必知道“未命中”在底层表现为哪个错误。示例使用占位参数,不拼接外部输入;这里的 db 是已经由应用初始化并共享的 *sql.DB。

package userrepo

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

type User struct {
    ID    int64
    Name  string
    Email string
}

func FindUser(ctx context.Context, db *sql.DB, id int64) (User, error) {
    var user User
    const query = `
        SELECT id, name, email
        FROM users
        WHERE id = ?`

    // 占位参数交给驱动绑定,避免把 id 拼进 SQL 文本。
    row := db.QueryRowContext(ctx, query, id)
    // QueryRow 的查询错误、空结果和字段转换错误都在 Scan 处返回。
    if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
        return user, fmt.Errorf("find user %d: %w", id, err)
    }
    return user, nil
}

这个函数有两个可复用的优点:查询上下文由调用方传入,可以响应请求取消或超时;错误使用 %w 包装,调用方仍能用 errors.Is 判断底层错误。

把三类错误与资源边界分开处理

实际项目不应把所有 Scan 错误都当成“用户不存在”。至少要区分:没有匹配行、目标字段类型不匹配,以及查询本身或上下文产生的错误。

命中记录、sql.ErrNoRows、Scan 类型错误、驱动或上下文错误与应用返回的静态关系说明图
图2:查询结果、错误分类、应用返回和 sql.DB 连接池的静态关系示意图,不是运行截图或执行证据。
func FindUser(ctx context.Context, db *sql.DB, id int64) (User, error) {
    var user User
    const query = `SELECT id, name, email FROM users WHERE id = ?`

    // QueryRowContext 适合最多返回一条记录的唯一键查询。
    err := db.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.Name,
        &user.Email,
    )
    switch {
    case err == nil:
        // 命中记录,返回完整对象。
        return user, nil
    case errors.Is(err, sql.ErrNoRows):
        // 空结果是正常业务分支,转换成调用方能理解的语义。
        return User{}, fmt.Errorf("user %d not found: %w", id, sql.ErrNoRows)
    default:
        // 类型转换、驱动、上下文和 SQL 错误不能伪装成未找到。
        return User{}, fmt.Errorf("query user %d: %w", id, err)
    }
}

上面的片段需要在导入区加入 errors。如果项目要返回自定义的 ErrUserNotFound,可以在业务层再包一层;关键是保留可判断的错误链,不要只把错误改成一段无法识别的字符串。

QueryRow 不需要 Rows.Close,但 DB 和 Stmt 仍有生命周期

QueryRowContext 返回的是 *sql.Row,调用方没有一个需要手动关闭的 *sql.Rows。所以不要为了“释放 QueryRow 资源”写不存在的 row.Close(),也不要把单行查询改成遍历循环来绕开错误处理。

需要管理的是更长生命周期的对象:

  • *sql.DB 通常代表连接池,由应用创建并复用;进程停止或该组件不再使用时调用 db.Close()。
  • 如果使用 PrepareContext 创建了 *sql.Stmt,应在语句生命周期结束时调用 stmt.Close()。
  • 如果改用 QueryContext 查询多行,必须按 rows.Close()、rows.Scan() 和 rows.Err() 的契约处理结果。
func prepareUserQuery(ctx context.Context, db *sql.DB) (*sql.Stmt, error) {
    // 预编译语句适合会被重复调用的固定 SQL。
    stmt, err := db.PrepareContext(ctx, `SELECT id, name, email FROM users WHERE id = ?`)
    if err != nil {
        return nil, fmt.Errorf("prepare user query: %w", err)
    }
    // 由调用方在整个使用周期结束后负责 stmt.Close。
    return stmt, nil
}

func FindUserWithStmt(ctx context.Context, stmt *sql.Stmt, id int64) (User, error) {
    var user User
    // Prepared Statement 也把查询错误延迟到 Scan。
    err := stmt.QueryRowContext(ctx, id).Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        return User{}, fmt.Errorf("find user with statement: %w", err)
    }
    return user, nil
}

一张表记住 QueryRow 的选择边界

现象推荐处理不要做什么
按唯一键查到一条QueryRowContext(...).Scan(...),返回对象先 Query 再手写只取第一行的循环
没有匹配记录用 errors.Is(err, sql.ErrNoRows) 转成领域语义把空结果误记成数据库故障
字段转换失败或驱动报错包装后向上返回,保留错误链统一改写成“记录不存在”
查询多行改用 QueryContext,处理 Rows 生命周期依赖 QueryRow 丢弃剩余行
复用预编译语句关闭 *sql.Stmt,调用 Stmt.QueryRowContext每次请求都创建而不关闭语句

常见疑问

QueryRow 返回 nil 时该怎么处理?

标准库的 QueryRow 和 QueryRowContext 会返回非 nil 的 *sql.Row;查询错误延迟到 Scan。因此应该检查 Scan 的错误,而不是围绕 row == nil 设计主流程。

为什么不能只判断 err == sql.ErrNoRows?

直接比较只适用于错误没有被包装的情况。仓储函数通常会给错误补充查询和主键上下文,所以调用方应使用 errors.Is,这样既能识别 sql.ErrNoRows,又不会丢掉上下文。

QueryRow 和 QueryRowContext 怎么选?

没有请求上下文时可以使用 QueryRow;服务端请求、后台任务和可取消操作优先使用 QueryRowContext,把取消和超时传到数据库调用。

QueryRow 能保证 SQL 只返回一行吗?

不能。它表达的是调用方只需要一行;如果查询实际匹配多行,Scan 使用第一行。需要唯一性保证时,仍应通过数据库约束或明确的查询条件保证数据模型成立。

记住一句话:QueryRowContext 负责表达“最多取一行”,Scan 负责交付结果和错误,sql.ErrNoRows 负责区分未命中,Rows.Close 只属于多行查询。把这四个概念分开,单行读取代码就不会再被资源释放和错误判断搅在一起。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go ReverseProxy 转发请求头的改写边界Go ReverseProxy 转发请求头的改写边界
上一篇
Go ReverseProxy 转发请求头的改写边界
Go database/sql DB.Conn 怎样为一次操作绑定独立连接
下一篇
Go database/sql DB.Conn 怎样为一次操作绑定独立连接
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    478次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    535次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    555次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    506次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    328次使用