Go database/sql 单行查询的 QueryRow 使用方式
在用户详情、配置读取、按主键查找这类场景里,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。
| 需求 | 入口 | 重点错误边界 |
|---|---|---|
| 最多一条记录 | QueryRowContext | Scan 读取错误,单独处理 sql.ErrNoRows |
| 需要遍历多条记录 | QueryContext | rows.Close()、rows.Scan() 和 rows.Err() |
| 反复执行同一 SQL | PrepareContext + Stmt.QueryRowContext | 语句创建失败与语句关闭错误 |
QueryRow 为什么把错误留到 Scan
QueryRowContext 的签名返回的是 *sql.Row,没有第二个 error。这不是它忽略了错误,而是把错误延迟到 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 错误都当成“用户不存在”。至少要区分:没有匹配行、目标字段类型不匹配,以及查询本身或上下文产生的错误。

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 只属于多行查询。把这四个概念分开,单行读取代码就不会再被资源释放和错误判断搅在一起。
Go ReverseProxy 转发请求头的改写边界
- 上一篇
- Go ReverseProxy 转发请求头的改写边界
- 下一篇
- Go database/sql DB.Conn 怎样为一次操作绑定独立连接
-
- Golang · Go教程 | 6分钟前 | Go crypto/tls 证书到期时间 x509.Certificate NotAfter
- Go crypto/tls 读取证书到期时间的代码写法
- 183浏览 收藏
-
- Golang · Go教程 | 9分钟前 |
- Go net/netip 解析地址并区分 IPv4 映射
- 377浏览 收藏
-
- Golang · Go教程 | 12分钟前 |
- Go container/heap 实现定时任务优先队列
- 395浏览 收藏
-
- Golang · Go教程 | 15分钟前 | Go 切片排序 稳定排序 sort.SliceStable
- Go sort.SliceStable 保持同键顺序的排序实现
- 300浏览 收藏
-
- Golang · Go教程 | 16分钟前 | 配置 · 错误处理 · Go教程 · Go 超时配置 time.Duration time.ParseDuration 配置时长
- Go time.ParseDuration 读取配置时长的错误处理
- 119浏览 收藏
-
- Golang · Go教程 | 18分钟前 | goroutine · go · Context · Go 定时任务 reset goroutine泄漏 time.Ticker Stop
- Go time.Ticker 长时间服务的停止与泄漏规避
- 121浏览 收藏
-
- Golang · Go教程 | 19分钟前 |
- Go time.Location 处理业务时区的转换方法
- 239浏览 收藏
-
- Golang · Go教程 | 27分钟前 |
- Go 构建标签拆分平台实现的组织方式
- 476浏览 收藏
-
- Golang · Go教程 | 28分钟前 |
- Go 私有模块下载的 GOPRIVATE 配置边界
- 284浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 478次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 535次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 555次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 506次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 328次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览
