当前位置:首页 > 文章列表 > Golang > Go教程 > Go database/sql 怎样区分 QueryRow 的空结果

Go database/sql 怎样区分 QueryRow 的空结果

来源:17golang原创 2026-10-18 21:07:46 0浏览 收藏

用 Go 的 database/sql 查询一条记录时,最容易写错的地方是把 QueryRow 当成了会立即返回错误的函数。它实际上总会返回一个非 nil 的 *sql.Row,没有匹配行、驱动错误和扫描错误,通常都要等到 Scan 才能看到。

因此,判断“没有结果”的标准不是检查 QueryRow,而是检查 Scan 返回的错误是否为 sql.ErrNoRows。业务层可以把它转换成“资源不存在”,其他错误则继续作为真正的查询失败处理。

先理解 QueryRow 的完整工作流

单行查询可以拆成三个阶段:构造查询、延迟执行结果、把列扫描到变量。QueryRow 负责返回一个占位的 Row,错误会暂存在这个对象里;调用 Scan 时,数据库结果才被映射到目标变量,错误也在这里统一返回。

QueryRow 返回 Row 后在 Scan 节点汇合空结果和查询错误的结构图
图1:QueryRow 返回 Row 后,空结果和查询错误在 Scan 节点汇合的静态结构图。

这也解释了为什么下面的写法没有意义:row := db.QueryRow(...) 之后不能从 row 本身判断是否存在记录,必须把它扫描出来。

用 sql.ErrNoRows 识别空结果

推荐在 Scan 返回后使用 errors.Is 判断。它表达的是“这个错误链中包含没有行”的语义,比只比较错误字符串可靠,也能兼容驱动或中间层对错误的包装。

package repository

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

type User struct {
	ID   int64
	Name string
}

// FindUser 只把“没有匹配行”转换成明确的 found=false,其他错误原样返回。
func FindUser(ctx context.Context, db *sql.DB, id int64) (User, bool, error) {
	var user User
	err := db.QueryRowContext(ctx,
		"SELECT id, name FROM users WHERE id = ?",
		id, // 参数通过占位符传递,避免把输入拼进 SQL 文本。
	).Scan(&user.ID, &user.Name)

	switch {
	case errors.Is(err, sql.ErrNoRows):
		// 空结果是正常的业务分支,不应伪装成数据库故障。
		return User{}, false, nil
	case err != nil:
		// 超时、连接、权限或扫描错误仍需交给上层处理。
		return User{}, false, err
	default:
		// Scan 成功后,user 已经填充了第一条匹配记录。
		return user, true, nil
	}
}

上层调用时可以把 found=false 映射成 HTTP 404、命令行提示或其他领域结果;不要把空结果和数据库故障混成同一个“查询失败”。

把空结果与其他错误分流

实际工程中,Scan 返回非 nil 并不只代表空结果。SQL 语法错误、连接中断、上下文超时、列类型不能转换,都会走错误分支。处理顺序应当是先识别 sql.ErrNoRows,再处理其余错误。

Scan 错误经过 errors.Is 分流到空结果、其他错误和成功结果的说明图
图2:Scan 返回值经过 errors.Is 判断后分到不存在、失败和成功三条处理支路的说明图。
func loadName(ctx context.Context, db *sql.DB, id int64) (string, error) {
	var name string
	err := db.QueryRowContext(ctx,
		"SELECT name FROM users WHERE id = ?", id,
	).Scan(&name)
	if errors.Is(err, sql.ErrNoRows) {
		// 用包级错误让服务层决定返回 404 还是其他业务结果。
		return "", ErrUserNotFound
	}
	if err != nil {
		// 这里保留原始错误,便于记录超时、驱动和扫描原因。
		return "", err
	}
	return name, nil
}

如果项目定义了自己的领域错误,建议用 fmt.Errorf("user %d: %w", id, ErrUserNotFound) 包装,然后仍然用 errors.Is 判断。这样日志可以保留上下文,调用者仍能识别稳定的错误类别。

不要把 NULL 当成没有行

sql.ErrNoRows 只表示结果集没有匹配记录。若记录存在,但某个列的值是 SQL NULL,这是另一种情况:目标变量需要能够表达“没有值”,例如 sql.NullString、sql.NullInt64 或项目自己的可空类型。

var nickname sql.NullString
err := db.QueryRowContext(ctx,
	"SELECT nickname FROM users WHERE id = ?", id,
).Scan(&nickname)
if err != nil {
	// 这里仍然先区分 ErrNoRows,再处理查询本身的失败。
	return err
}
if !nickname.Valid {
	// 有这一行,只是 nickname 的 SQL 值为 NULL。
	useDefaultNickname()
} else {
	useNickname(nickname.String)
}

如果直接把 SQL NULL 扫描到普通 string,可能得到扫描错误;这个错误也不能被当成空结果。

QueryRow 的两个边界

第一,QueryRow 适合“最多需要一行”的查询。官方语义是扫描第一条匹配记录并丢弃其余记录,所以它不会帮你判断数据是否意外重复。如果业务要求必须唯一,应当在数据库加唯一约束,或改用 QueryContext 自己检查行数。

第二,QueryRow 的错误只能在 Scan 后判断。下面这种判断既捕获不到空结果,也无法区分其他查询错误:

row := db.QueryRowContext(ctx, query, id)
// 错误不在 row == nil 中体现;QueryRow 成功返回的 Row 通常非 nil。
if row == nil {
	return ErrUserNotFound
}

正确做法是把 QueryRowContext(...).Scan(...) 放在同一个错误处理点,并让调用方明确区分三种结果:找到记录、没有记录、查询失败。

一张表记住判断方式

Scan 结果含义建议处理
nil至少扫描到一行,目标值可用返回实体或成功结果
errors.Is(err, sql.ErrNoRows)没有匹配行转换为 NotFound 等业务分支
其他非 nil 错误查询、上下文、驱动或扫描失败保留并上报原始错误
nil 但可空字段无效记录存在,某列是 SQL NULL检查 sql.Null* 的 Valid

常见问题

为什么 QueryRow 不直接返回 ErrNoRows?

因为它返回的是延迟读取的 Row。只有调用 Scan 时,程序才知道需要哪些目标列并完成结果映射,所以空结果错误在 Scan 阶段返回。

可以直接写 err == sql.ErrNoRows 吗?

简单代码可以这样写,但工程代码更适合 errors.Is,尤其是错误可能经过包装时。不要比较错误字符串。

QueryRow 查到多行会报错吗?

不会自动因为多行而报错;它扫描第一行并忽略其余行。需要唯一性保证时,应把约束放在数据库或改用多行查询自行检查。

结语

记住一个判断点就够了:QueryRow 本身不报告空结果,Scan 才是错误汇合点;用 errors.Is(err, sql.ErrNoRows) 识别没有行,再把 SQL NULL、查询失败和多行边界分别处理,代码的业务语义会清晰很多。

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

Go 数据库查询指南:https://go.dev/doc/database/querying

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