GORM 的 Version 字段需严格满足:字段名必须为 Version、类型为 int/uint、位于结构体一级、数据库列类型匹配(如 MySQL 用 INT),且仅在 Save/Updates 等 GORM 方法中自动参与乐观锁校验,手动 SQL 或修改 Version 值将使其失效。

为什么 GORM 的 Version 字段不自动生效
GORM 的乐观锁不是开箱即用的魔法,它依赖你显式启用且字段名必须叫 Version(类型为 int 或 uint),否则哪怕你加了标签也无效。常见错误是自定义字段名如 version_num 或用 string 类型存版本号——GORM 完全忽略。
-
Version字段必须是结构体一级字段,不能嵌套在匿名结构里 - 迁移时需确保数据库列类型匹配:MySQL 用
INT,PostgreSQL 用INTEGER,别用BIGINT(GORM v1.25+ 对BIGINT支持不稳定) - 更新操作必须走
Save或Updates,直接写 SQL 或用Exec绕过 GORM 就不会触发版本检查
GORM 更新时如何触发版本校验
只有当调用 Save、Updates 或 UpdateColumns 且目标结构体含未修改的 Version 值时,GORM 才会在 WHERE 条件中加入 version = ?。这意味着:如果你先 Find 出一条记录,改完其他字段再 Save,GORM 自动把原 Version 值塞进 WHERE;但如果手动给 Version 赋了新值,它就失效了。
- 安全做法:始终从 DB 查出完整记录再更新,不要 new 一个结构体后只填几个字段去
Save - 避免手动修改
Version字段值,GORM 会在成功更新后自动 +1 - 如果用
Updates(map[string]interface{}),GORM 不会读取原Version,得自己拼 WHERE:db.Where("version = ?", old.Version).Updates(...)
更新失败后怎么判断是乐观锁冲突
GORM 不抛出特定异常,而是返回 RowsAffected == 0。你不能只看 error 是否为 nil —— 冲突时 error 是 nil,但没更新任何行。典型错误是写成 if err != nil { ... } 就认为成功,结果静默跳过冲突。
- 必须检查
result.RowsAffected,为 0 即表示版本不匹配(或记录不存在) - 区分“记录不存在”和“版本冲突”需要额外逻辑:先查一次主键是否存在,或在 WHERE 中同时加
id = ? AND version = ? - 重试时别忘了重新
Find最新数据,否则下次还是拿旧Version去撞墙
并发更新下 Version 字段的竞态风险点
版本号递增本身由 GORM 在 UPDATE 语句中完成(如 SET version = version + 1),不依赖 Go 层读-改-写,所以不会因 goroutine 并发导致覆盖。但问题出在业务逻辑层:比如两个请求都查到 Version=5,各自计算新状态后尝试更新,只有一个能成功,另一个得重试。这时候容易忽略的是「重试边界」——无限循环重试可能卡死,或没控制最大尝试次数。
立即学习“go语言免费学习笔记(深入)”;
- 建议加简单重试计数(如最多 3 次),超限后返回
http.StatusConflict - 别在事务外做重试:每次重试都应开启新事务,否则可能读到脏数据
- 如果业务允许最终一致,可考虑用
SELECT FOR UPDATE替代乐观锁,但会降低并发度
真正麻烦的从来不是加个 Version int 字段,而是所有读写路径都得统一走 GORM 的生命周期,漏掉任意一处直连 SQL 或缓存绕过,乐观锁就形同虚设。










