Go 中死锁由运行时检测并 panic 报“all goroutines are asleep - deadlock”,仅当所有 goroutine 均阻塞且无可运行态时触发;数据库死锁则由 DB 层(如 MySQL 错误码 1213)返回,需 Go 程序捕获重试。

Go 应用里死锁不会被 database/sql 自动检测
Go 标准库的 database/sql 包本身不感知数据库层面的死锁,它只负责转发 SQL 请求和返回错误。真正检测并中断死锁的是底层数据库(如 MySQL、PostgreSQL),而 Go 程序能做的,是捕获数据库返回的特定死锁错误码,并做重试或降级处理。
常见误区是以为加个超时就能“检测”死锁——其实超时只能防止无限等待,但无法区分是真死锁、网络延迟还是慢查询。必须依赖数据库明确抛出的死锁错误信号。
- MySQL 死锁错误码是
1213,错误信息通常含"Deadlock found when trying to get lock" - PostgreSQL 死锁错误码是
40P01(SQLSTATE),错误信息含"deadlock detected" - SQLite 不支持并发写事务死锁检测(WAL 模式下可能报
SQLITE_BUSY,但非标准死锁)
如何可靠识别并捕获死锁错误
不能靠字符串匹配错误信息(易受本地化、版本变更影响),应优先检查错误码或 SQLSTATE。使用 errors.As 提取驱动特定错误类型是最稳妥的方式。
以 mysql 驱动为例:
var mySQLError *mysql.MySQLError
if errors.As(err, &mySQLError) && mySQLError.Number == 1213 {
// 是 MySQL 死锁
}
对 pgx(PostgreSQL):
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "40P01" {
// 是 PostgreSQL 死锁
}
- 务必在
Exec/Query后立即检查err,不要延迟到 Scan 或 Close 阶段 - 避免用
fmt.Sprintf("%v", err)做判断——会丢失原始错误类型 - 如果用 ORM(如 GORM),需查其文档是否暴露底层错误;GORM v2 默认会包装错误,要用
errors.Unwrap或GormError类型断言
自动重试逻辑该加在哪里、加几次
死锁是瞬态冲突,重试往往有效,但必须控制边界:不能无限重试,也不能在非死锁错误上盲目重试。
- 只对确认的死锁错误重试,其他错误(如约束失败、连接中断)应直接返回
- 建议最多重试 2–3 次,每次加小随机抖动(如 10–100ms),避免重试洪峰再次撞死锁
- 重试应包裹整个事务块,而非单条语句(否则事务状态已损坏)
- 若业务允许,可把高冲突操作拆成更小粒度事务,降低死锁概率
示例节选(事务内重试):
for i := 0; i <h3>容易被忽略的底层细节</h3><p>死锁检测不是纯应用层问题,很多表现看似“Go 死锁”,实则是数据库配置或 SQL 写法导致的假象。</p>
- MySQL 的
innodb_lock_wait_timeout默认 50 秒——若你的 Gocontext.WithTimeout设为 30 秒,那根本等不到数据库报死锁,只会收到"context deadline exceeded" - 没加索引的
WHERE条件会导致全表扫描锁,极大提升死锁概率;务必用EXPLAIN确认执行计划 - 不同事务按不同顺序更新多行(如事务 A 先更新 user1 再 user2,事务 B 反之),是经典死锁诱因——应用层应强制统一更新顺序(如按 ID 升序)
- 长事务(>1s)显著增加冲突窗口,应尽早提交,避免在事务中做 HTTP 调用、文件读写等阻塞操作
真正难的从来不是“怎么重试”,而是怎么让数据库少产生死锁——这需要结合慢日志、锁监控(如 information_schema.INNODB_TRX)和业务流程一起看。











