Go 的 database/sql 错误不为 nil 不一定代表失败,需区分错误来源:Query/QueryRow 的 err 仅表示 SQL 提交是否成功,rows.Next() 返回 false 后须调用 rows.Err() 判断是否真出错,Scan 和 Close 也可能返回 error,事务中 Rollback 和 Commit 均需显式检查错误。

Go 的 database/sql 错误不是 nil 就代表失败?
很多刚写 Go 数据库代码的人会误以为只要 err != nil 就一定出错了,其实不然。比如调用 rows.Next() 返回 false 可能只是查不到数据,也可能是底层驱动报错;而这个错误要等到你调用 rows.Err() 才能拿到。漏掉这一步,就会让 SQL 执行失败却“静默”通过。
正确做法是:每次遍历完 rows 后必须检查 rows.Err(),且不能只依赖 QueryRow().Scan() 的返回 err —— 它只捕获查询发起阶段的错误(如语法错、连接断),不包含扫描时的类型不匹配或空值处理异常。
-
Query()和QueryRow()返回的err仅表示“SQL 是否成功提交”,不代表结果能正常读取 -
rows.Scan()失败不会自动终止循环,需在每次调用后检查其返回 err -
rows.Close()也可能返回 error(如网络中断导致清理失败),建议 defer 后仍做一次显式检查
如何区分 sql.ErrNoRows 和其他数据库错误?
sql.ErrNoRows 是唯一一个被标准库明确定义的“预期性错误”,它不是 bug,而是业务常态(例如查用户不存在)。直接用 if errors.Is(err, sql.ErrNoRows) 判断最稳妥,避免字符串匹配或 == 比较 —— 不同驱动返回的具体 error 类型可能不同(如 pgx 返回的是自定义类型)。
var user User
err := db.QueryRow("SELECT name, age FROM users WHERE id = $1", 123).Scan(&user.Name, &user.Age)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 用户不存在,业务逻辑继续
return nil, nil
}
// 其他错误:字段类型不匹配、NULL 写入非指针等
return nil, fmt.Errorf("query user: %w", err)
}
return &user, nil
注意:只有 QueryRow().Scan() 会返回 sql.ErrNoRows;Query() + rows.Next() 不会,它只会让 Next() 返回 false,此时你要靠 rows.Err() 判断是否真出错。
立即学习“go语言免费学习笔记(深入)”;
事务中遇到错误,tx.Rollback() 为什么还 panic?
常见写法是 defer tx.Rollback() 然后在中间 commit,但这样非常危险:如果 Rollback() 自己失败(比如连接已断),会触发 panic。更糟的是,panic 可能掩盖原本的业务错误。
正确模式是显式控制 rollback,并只在未 commit 时才调用:
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
// 执行操作...
_, err = tx.Exec("INSERT INTO orders (...) VALUES (...)", ...)
if err != nil {
tx.Rollback() // 显式回滚
return fmt.Errorf("insert order: %w", err)
}
err = tx.Commit()
if err != nil {
tx.Rollback() // commit 失败也要尝试回滚
return fmt.Errorf("commit transaction: %w", err)
}
关键点:
- 不要用
defer tx.Rollback()无条件回滚,它无法区分“该回滚”和“已经 commit” -
tx.Commit()和tx.Rollback()都可能返回 error,尤其是网络不稳定时 - 如果使用
sqlx或ent等 ORM,它们内部通常已封装了安全的事务控制,但底层逻辑仍遵循这一原则
用 database/sql 处理 NULL 值时最容易踩的坑
Go 的原生 Scan 不支持直接把数据库 NULL 映射到普通类型(如 int、string),强行这么做会导致 sql.ErrNoRows 以外的 panic 或静默截断。必须用指针或 sql.Null* 类型。
推荐优先使用 sql.NullString、sql.NullInt64 等,它们自带 Valid 字段,语义清晰:
var name sql.NullString
var age sql.NullInt64
err := row.Scan(&name, &age)
if err != nil {
return err
}
if name.Valid {
fmt.Println("Name:", name.String)
} else {
fmt.Println("Name is NULL")
}
如果你用 struct + Scan,别忘了字段必须是导出的(首字母大写),且类型要严格匹配 —— sql.NullString 不能 Scan 进 *string,反之亦然。
另一个隐形陷阱:SELECT COALESCE(col, '') FROM t 看似绕过 NULL,但会丢失原始列的 NULL 语义,影响后续聚合或索引优化,不如在 Go 层明确处理。
if err != nil,而是想清楚这个 err 到底来自哪一层:驱动连接?SQL 解析?权限校验?类型转换?还是事务状态不一致?每种都需要不同的恢复策略。










