应使用 errors.Is(err, sql.ErrNoRows) 显式判断查询无数据,而非 err == sql.ErrNoRows 或泛化错误处理;仅 QueryRow/QueryRowContext 返回该错误,Query 需用 rows.Next() 判断。

如何判断 sql.ErrNoRows 而不是泛化地用 errors.Is 捕获所有错误
数据库查询没找到数据时,db.QueryRow().Scan() 会返回 sql.ErrNoRows,这是预期行为,不是异常。直接用 if err != nil 判定会导致误报业务错误。
- 必须用
errors.Is(err, sql.ErrNoRows)显式识别,不能用err == sql.ErrNoRows—— 因为底层可能包装过 - 若想忽略“无数据”并返回零值,可直接写
if errors.Is(err, sql.ErrNoRows) { return 0, nil } - 注意:只有
QueryRow和QueryRowContext会返回sql.ErrNoRows;Query返回空*sql.Rows,需靠rows.Next()判断
driver.ErrBadConn 的真实用途与过时风险
这个错误曾用于提示连接失效,让调用方重试。但自 Go 1.8 起,database/sql 已内置连接健康检查和自动重试逻辑,driver.ErrBadConn 不再被上层识别为重试信号。
- 现代驱动(如
pgx、mysql)已弃用该错误;若你还在代码里手动判断它,大概率是旧文档或遗留逻辑 - 真正需要重试的场景(如网络闪断),应依赖
context.WithTimeout+ 外层重试控制,而非解析底层驱动错误 - 如果驱动仍返回
driver.ErrBadConn,建议升级驱动版本,或改用errors.Is(err, driver.ErrBadConn)后直接记录日志,不再做恢复动作
事务中遇到 sql.ErrTxDone 怎么办
调用 tx.Commit() 或 tx.Rollback() 后,该事务对象即进入完成状态。后续再对它执行 tx.Query 或 tx.Exec 就会返回 sql.ErrTxDone —— 这是明确的使用错误,不是并发问题。
- 常见诱因:defer 中写了
tx.Rollback(),但前面已显式Commit(),导致重复操作 - 正确模式是用
if tx == nil或标志位控制 rollback 是否执行,或者统一用defer func() { if r := recover(); r == nil && !committed { _ = tx.Rollback() } }() - 不要试图恢复
sql.ErrTxDone,它表示你已经失去了对该事务的控制权,必须新建事务重试
func updateUser(tx *sql.Tx, id int, name string) error {
_, err := tx.Exec("UPDATE users SET name = ? WHERE id = ?", name, id)
if err != nil {
return err
}
// 此处若误加 tx.Rollback(),下次再调用 tx.Exec 就会触发 sql.ErrTxDone
return nil
}
为什么 errors.As 在数据库错误中很少用得上
errors.As 用于提取底层错误类型(比如把包装后的错误转成 *pq.Error),但它只在你需要读取数据库特有字段(如 PostgreSQL 的 Code、Detail)时才有意义。多数业务层只需分类处理,不需要深入驱动细节。
- PostgreSQL 场景下,若要区分唯一约束冲突(
23505)和外键失败(23503),才需errors.As(err, &pqErr) - MySQL 驱动目前不导出错误结构体,
errors.As无法提取具体 SQLSTATE,只能靠err.Error()字符串匹配(不推荐) - 跨数据库项目应避免依赖驱动特定错误,优先用
errors.Is判断通用语义错误(如sql.ErrNoRows、sql.ErrTxDone)
log.Fatal 或往上 panic。










