GORM乐观锁需显式启用插件并使用Model().Update()方法,仅定义Version字段无效;Save/FirstOrCreate等方法及原生SQL均不触发版本校验,冲突时仅返回通用错误。

gorm 乐观锁插件必须显式启用
直接在模型里加 Version 字段不会自动生效,GORM 默认不启用乐观锁逻辑。你得手动注册插件,并确保调用链经过它处理的 Update 或 Save 方法。
常见错误是只定义了字段但没初始化插件,结果更新语句里压根不带 WHERE version = ? 条件,完全退化成普通写入。
- 安装插件:
go get -u gorm.io/plugin/optimisticlock - 初始化时注册:
db.Use(optimisticlock.New(optimisticlock.Config{})) - 模型中字段类型必须是
optimisticlock.Version,不能是int或uint—— 否则插件无法识别并注入校验逻辑
Update 和 Save 的行为差异很关键
db.Model(&user).Update("name", "xxx") 会生成带版本号校验的 SQL;但 db.Save(&user) 不会——它走的是全量覆盖逻辑,即使字段有变更也不会检查版本。
这是最容易踩的坑:你以为用了乐观锁,实际只是在做无条件更新。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 必须用
Model().Update()或Model().Updates()才触发乐观锁拦截器 -
Save()、Create()、FirstOrCreate()等方法均绕过乐观锁机制 - 如果要批量更新多个字段,用
Updates(map[string]interface{}),别用结构体赋值后Save
冲突发生时返回的错误不包含具体原因
GORM 乐观锁冲突时只返回一个通用的 gorm.ErrRecordNotFound,而不是明确说“version mismatch”。你无法仅靠错误类型判断是不是并发冲突,必须结合业务逻辑做二次验证。
比如用户提交订单时库存扣减失败,你得查一遍当前数据库里的 version 和 stock 值,确认是版本不一致还是库存已为零。
- 不要直接
if errors.Is(err, gorm.ErrRecordNotFound)就认定是乐观锁失败 - 建议在更新前先
SELECT ... FOR UPDATE(悲观锁)做兜底,或用重试 + 指数退避策略 - 日志里至少打印出被操作记录的
ID和当前version,方便排查到底是哪次写入赢了
原生 SQL 和 Raw Query 不受乐观锁保护
一旦你用 db.Exec("UPDATE products SET stock = ? WHERE id = ?", newStock, id),无论模型有没有 Version 字段,都不会有任何版本校验。
这在需要高性能批量更新或复杂条件更新时很常见,但容易让人误以为“我已经上了乐观锁”,结果在关键路径上裸奔。
- 所有绕过 GORM ORM 层的写操作,都等于手动关掉了乐观锁
- 如果必须用原生 SQL,请自行拼接
AND version = ?并管理version变量的读-改-写流程 - 注意:
db.Transaction()内部用原生 SQL 也一样失效,事务本身不继承乐观锁能力

















