beforecreate和beforeupdate是gorm校验入口,返回error可中断事务;须用指针接收者、正确签名、同包定义,校验失败需return error,成功须return nil,字段填充宜用tx.statement.setcolumn避免覆盖。

BeforeCreate 和 BeforeUpdate 钩子是校验入口
GORM 没有内置 validator,但 BeforeCreate 和 BeforeUpdate 是最自然的校验时机——它们在写入前触发,且返回 error 会直接中断事务。别用 BeforeSave 替代,它同时覆盖创建和更新,容易让业务逻辑耦合、难以单独测试。
常见错误现象:BeforeCreate 中修改了字段但没调用 tx.Statement.SetColumn,导致新值未生效;或校验失败后仍继续执行,是因为忘了 return error。
- 只在校验失败时 return error,成功必须显式 return nil
- 若需自动填充(如生成 UUID、计算哈希),应在校验通过后操作
- 钩子方法签名必须是
func(*Model) error或func(*Model, *gorm.DB) error,否则 GORM 不识别
字段级校验要区分空值语义
Go 的零值(""、0、nil)不等于“未提供”,尤其对可空字段(如 *string、sql.NullString)。直接判空会误杀合法的 0 值或空字符串。
使用场景:用户允许年龄为 0(新生儿)、邮箱可为空、但手机号必填且需格式校验。
- 对指针类型字段(如
*string),用u.Email != nil && *u.Email == ""判断是否为空字符串 - 对非指针字段(如
Age int),用额外标记字段(如AgeSet bool)或改用sql.NullInt32 - 避免在钩子里重复解析 JSON 或做耗时正则匹配,提取成预编译变量(如
var emailRegex = regexp.MustCompile(...))
错误信息要能定位到具体字段
返回 errors.New("用户名不能为空") 在调试时很弱——不知道是哪个模型、哪次请求、哪个字段出问题。GORM 不会自动关联字段名,得靠自己组织。
性能影响:拼接字符串本身开销小,但若日志量大且含敏感数据(如密码明文),可能引发安全或存储问题。
- 推荐返回结构化错误,例如
fmt.Errorf("user.name: %w", errors.New("cannot be empty")) - 配合日志中间件,在 DB 层统一记录
tx.Statement.Table和tx.Statement.SQL.String() - 不要在错误里暴露内部逻辑(如“违反唯一索引”),应转为业务语言(如“该邮箱已被注册”)
复杂规则建议抽离成独立函数
当校验逻辑涉及查库(如检查邮箱是否已存在)、调外部服务(如实名认证)、或多个字段交叉判断(如“结束时间必须晚于开始时间”),把它们从钩子中拆出来,保持钩子轻量。
容易踩的坑:在 BeforeCreate 里调用 tx.First() 查自身表,可能因事务隔离级别导致查不到刚插入但未提交的数据;或循环依赖其他钩子造成死锁。
- 查库类校验,用传入的
tx *gorm.DB实例,确保同事务上下文 - 跨字段验证(如 StartAt/EndAt),在钩子内完成,不要延迟到 service 层
- 第三方调用必须加 context 和超时,钩子默认无超时控制,失败会卡住整个 DB 操作
真正难的不是写校验逻辑,而是决定哪些规则该放在钩子里、哪些该前置到 HTTP 层(如 Gin 的 binding 标签)、哪些该交给数据库约束(如 NOT NULL、UNIQUE)。三者边界模糊,但越靠近数据写入点的校验,越难绕过、越可靠。











