sql.errnorows不算业务错误,是queryrow().scan()查无结果的正常哨兵错误,须显式用err == sql.errnorows判断,不可与连接失败等错误混为一谈。

遇到 sql.ErrNoRows 到底算不算错误?
不算业务错误,是查询无结果的正常信号。Go 的 database/sql 把“没查到”和“连接失败”“语法错误”严格区分开——sql.ErrNoRows 是一个预定义的、可预期的哨兵错误,专门用来表示 QueryRow().Scan() 没匹配到任何行。
常见错误现象:直接用 if err != nil 统一处理,结果把“查不到用户”当成严重故障打日志、返回 500;或者忽略它,导致 Scan() 后变量仍是零值却误以为数据存在。
- 必须显式判断
err == sql.ErrNoRows,不能只看err != nil - 只要用了
QueryRow()+Scan(),就必须处理这个分支,哪怕只是返回nil或默认值 - 它只在
QueryRow().Scan()中触发;Query()返回*Rows,需要自己调rows.Next()判断是否为空
QueryRow().Scan() 的典型安全写法
核心原则:先判 sql.ErrNoRows,再判其他错误。顺序反了会导致空指针或逻辑错乱。
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", userID).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
return "", nil // 或返回自定义的 NotFound 错误
}
return "", fmt.Errorf("db query failed: %w", err) // 其他错误才向上抛
}
return name, nil
-
Scan()必须传指针,传值会导致 panic(panic: sql: Scan error on column index 0: destination not a pointer) - 如果 SQL 查询字段数与
Scan()参数个数不一致,会报sql.ErrNoRows以外的错误,比如sql: expected 1 destination arguments, got 2 - 扫描到
NULL值时,目标变量不会被修改(保持零值),但不会触发sql.ErrNoRows;需用sql.NullString等类型显式接收
什么时候不该用 sql.ErrNoRows?
它只属于 QueryRow() 场景。其他常见操作有各自的行为模式:
立即学习“go语言免费学习笔记(深入)”;
-
Exec()和Query()永远不会返回sql.ErrNoRows;前者返回影响行数,后者返回*Rows对象 -
QueryRowContext()也返回sql.ErrNoRows,但超时或取消会返回context.DeadlineExceeded或context.Canceled,要分开处理 - 批量插入/更新用
Exec(),即使影响 0 行也不等于出错;只有Result.RowsAffected()返回非零值才说明有数据变动
容易被忽略的兼容性细节
sql.ErrNoRows 是标准库导出的变量,不是字符串,所以不能用 strings.Contains(err.Error(), "no rows") 判断——不同驱动实现可能输出不同文本,且未来可能变更。
- 永远用
err == sql.ErrNoRows做精确比较,这是 Go 官方明确保证的语义 - 如果你封装了数据库层,别把它转成自定义错误再包裹,否则上游无法再用
==判断;要用errors.Is(err, sql.ErrNoRows)包装后仍保留原始哨兵 - SQLite 驱动(
mattn/go-sqlite3)和 PostgreSQL 驱动(lib/pq或jackc/pgx)都遵循这一约定,无需额外适配
真正麻烦的是嵌套查询场景:比如外层 QueryRow() 查用户,内层又调另一个 QueryRow() 查关联配置——两层都要独立判断 sql.ErrNoRows,漏一层就可能让空值穿透到业务逻辑里。










