钩子不触发需检查接收者和包路径:必须指针接收者、同包、*User类型;Save()跳过钩子,改用Create/Update;时间戳更新用tx.Statement.SetColumn;软删除无AfterDelete,清理逻辑放AfterUpdate并判DeletedAt非空。

钩子不触发?先看接收者和包路径对不对
90% 的钩子静默失效,是因为方法签名或定义位置错了。GORM v2 只认 func (u *User) BeforeCreate(tx *gorm.DB) error 这种格式:必须是指针接收者、带 tx *gorm.DB 参数、返回 error。
常见错误包括:
- 写成值接收者:
func (u User) BeforeCreate(...)→ 不触发 - 把钩子放在
hooks/子目录或独立的package hooks里 → 包不一致,GORM 直接忽略 - 模型用了别名类型(如
type Admin User)→ 钩子不会继承,得单独为Admin实现
务必确认钩子和 User struct 在同一个 package 下,且接收者是 *User。
Save() 为什么总绕过钩子?改用 Update() 或 Updates()
Save() 是 GORM 中少数明确设计为跳过所有钩子的操作——它不走 BeforeUpdate、BeforeCreate,也不走验证逻辑,只做“直写”。想让时间戳、加密、ID 生成等逻辑生效,必须避开它。
立即学习“go语言免费学习笔记(深入)”;
正确做法:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 新增用
db.Create(&user)→ 触发BeforeCreate - 局部更新用
db.Model(&user).Update("status", "done")或Updates(map[string]interface{}{"status": "done"})→ 触发BeforeUpdate - 真要用
Save()?加Session显式启用钩子:db.Session(&gorm.Session{SkipHooks: false}).Save(&user),但不推荐,语义不清
BeforeUpdate 里改时间戳,别直接赋值 u.UpdatedAt
直接写 u.UpdatedAt = time.Now() 看似简单,实则危险:GORM 会把整个 struct 当作待更新字段,导致你本意只更新 status,结果 CreatedAt 也被覆盖。
安全写法是只影响 SQL 构建,不污染内存中的 struct:
- 用
tx.Statement.SetColumn("updated_at", time.Now())→ 只修改即将生成的 SQL 字段值 - 若要排除某些字段(比如不让
CreatedAt被更新),配合Omit("created_at")或Select("status", "updated_at") - 字段类型必须是
*time.Time(指针)才能在钩子里真正改到原值;用time.Time值类型只会改副本
软删除时钩子顺序反直觉:AfterDelete 不会执行
启用软删除后,Delete() 实际走的是 BeforeDelete → 更新 DeletedAt → AfterUpdate,不是 AfterDelete。很多人在这里卡住:日志没打、清理逻辑没跑,还以为钩子坏了。
关键判断点:
- 在
BeforeDelete里,u.DeletedAt还是零值,不能靠它判断是否已软删 - 区分硬删和软删,得查
tx.Statement.Unscoped是否为true,或者约定硬删必须显式调用Unscoped().Delete() - 清理关联数据(如软删用户后禁用其地址),应放在
AfterUpdate里,并检查u.DeletedAt != nil
钩子不是魔法,它是同步阻塞的事务内逻辑。一旦在 Before* 里返回非 nil error,整个操作立刻回滚;而 After* 出错,事务已提交,只能靠补偿处理——这点容易被忽略,但直接影响数据一致性。

















