应通过 errors.as 类型断言提取具体错误:postgresql 用 pgconn.pgerror.sqlstate()=="23505",mysql 用 mysql.mysqlerror.number==1062;超时需区分 context.deadlineexceeded、sqlstate "57014" 或错误码 1317 等不同层级;事务中唯一冲突后必须 rollback 再重试。

怎么从 pg.Error 或 mysql.MySQLError 里准确识别唯一键冲突
Go 的数据库驱动对错误的封装程度不同,但核心思路一致:不能靠 err.Error() 字符串匹配,得用类型断言+错误码判断。
PostgreSQL 驱动(github.com/jackc/pgx/v5)返回的是 *pgconn.PgError,它的 SQLState() 是关键:
-
"23505"表示唯一约束违反(包括主键、唯一索引) -
"23503"是外键约束失败,别混淆
MySQL 驱动(github.com/go-sql-driver/mysql)需断言为 *mysql.MySQLError,检查 Number:
-
1062是重复键错误(唯一/主键冲突) -
1205是死锁,不是超时,别误判
示例判断逻辑:
立即学习“go语言免费学习笔记(深入)”;
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.SQLState() == "23505" {
// 处理唯一键冲突
}
if myErr, ok := err.(*mysql.MySQLError); ok && myErr.Number == 1062 {
// 处理 MySQL 唯一键冲突
}
超时错误为什么不能只看 context.DeadlineExceeded
数据库操作超时可能发生在两个层面:SQL 执行层(如 statement_timeout)和连接层(如 net.DialTimeout),它们触发的错误类型完全不同。
常见误区是只检查 errors.Is(err, context.DeadlineExceeded),但这只覆盖了显式传入 context.WithTimeout 后主动取消的情况;而 PostgreSQL 的 statement_timeout 触发的是 SQLState() == "57014"(查询被取消),MySQL 的 wait_timeout 断连则抛出 io.EOF 或 "i/o timeout" 字符串错误(无法用 errors.Is 精确匹配)。
- 连接池获取连接超时 → 通常是
sql.ErrConnDone或底层net.OpError带"timeout"字段 - 查询执行超时(服务端强制中断)→ PostgreSQL 返回
"57014",MySQL 返回1317(query execution was interrupted) - 客户端上下文超时 → 才是
context.DeadlineExceeded
用 errors.As 还是 errors.Is 判断数据库错误
必须用 errors.As 提取具体错误类型,errors.Is 只适用于已知的哨兵错误(比如 sql.ErrNoRows),对驱动自定义错误几乎无效。
因为各驱动返回的错误是结构体指针,且未实现 Unwrap 或未嵌套标准哨兵值。例如 pgx 的 PgError 不会 Unwrap() 成 context.DeadlineExceeded,即使它由超时引发。
- 判断唯一冲突 →
errors.As(err, &pgErr)+ 检查pgErr.SQLState() - 判断连接断开 →
errors.As(err, &netErr)+netErr.Timeout() - 判断无结果 → 只有
errors.Is(err, sql.ErrNoRows)安全,这是标准包定义的哨兵
为什么在事务里捕获唯一冲突后不能直接重试插入
PostgreSQL 在唯一冲突后,当前事务状态变成 aborted,后续任何语句都会报 "current transaction is aborted, commands ignored until end of transaction block"(SQLState "25P02")。MySQL 虽然没这个严格状态,但若在事务中发生唯一冲突,再执行其他 DML 可能因隔离级别导致幻读或不一致。
- 必须先
tx.Rollback(),再重新开始事务 - 不要在同一个
tx实例上调用多次Exec后才检查错误 - 如果想“存在则更新”,优先用
ON CONFLICT DO UPDATE(PG)或INSERT ... ON DUPLICATE KEY UPDATE(MySQL),而不是手动捕获+重试
最易被忽略的一点:错误分类代码往往写在 DAO 层,但事务生命周期控制在 service 层——这意味着错误判断逻辑必须能透传状态,不能让 DAO 自行 rollback,否则会破坏上层事务边界。










