软删除字段必须命名为DeletedAt且类型为*time.Time,GORM仅识别该字段名和类型实现软删;Delete()默认UPDATE设DeletedAt,真删需Unscoped();关联数据不自动软删,需手动处理。

软删除字段必须叫 DeletedAt,且类型为 *time.Time
GORM 的软删除机制是硬编码识别的:只有字段名恰好是 DeletedAt 且类型为 *time.Time,才会被自动视为软删除标记。其他命名(比如 IsDeleted、deleted_at)或类型(如 bool、int)都不会触发 GORM 的软删除逻辑——它不会帮你查、不会自动过滤、也不会在 Delete() 时做 UPDATE。
实操建议:
- 结构体中必须定义
DeletedAt gorm.DeletedAt(gorm.DeletedAt是*time.Time的别名,自带零值和扫描逻辑) - 不要用
gorm.Model以外的嵌入方式绕过,否则可能丢失DeletedAt字段注册 - 如果已有表结构含自定义软删字段(如
is_deleted TINYINT),GORM 无法接管,只能手动写 WHERE 条件
db.Unscoped().Delete() 才是真删,漏掉 Unscoped 就只是软删
调用 Delete() 默认走软删除流程:GORM 生成 UPDATE 语句,把 DeletedAt 设为当前时间。你看到“删除成功”,但数据仍在数据库里。真正物理删除必须显式加 Unscoped()。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 后台管理点了“彻底删除”,但数据还在表里 —— 忘了
Unscoped() - API 返回列表时出现已“删”数据 —— 查询没加
Unscoped(),但本意是查全部(含已软删) - 用
First()查单条时查不到刚Delete()的记录 —— 正常,软删后默认不查
示例:
// 软删除(UPDATE)
db.Delete(&user)
// 真删(DELETE FROM)
db.Unscoped().Delete(&user)
// 查所有,含已软删的
db.Unscoped().Where("id = ?", 123).First(&user)
Gin 中返回软删数据前,务必确认是否要暴露 DeletedAt 字段
Go 结构体默认导出字段都会被 JSON 序列化。如果结构体含 DeletedAt,且值非 nil,API 响应里就会出现 "deleted_at":"2024-05-20T10:30:00Z" —— 大部分前端不关心这个,还可能引发权限误解(“这用户明明删了怎么还能登录?”)。
使用场景与处理建议:
- 管理后台需展示“已删除”状态:保留字段,但改名输出,例如用
json:"deleted_at,omitempty,string"控制格式 - 用户侧 API(如 /api/user/me):结构体用匿名嵌入 + 字段屏蔽,或定义专用响应结构体,不包含
DeletedAt - 用
SELECT *查库再直接c.JSON(200, user)最危险 —— 很可能意外泄露软删状态
软删除不是银弹:关联数据不会级联软删,得自己处理
GORM 的软删除只作用于当前模型。比如 User 软删了,它的 Orders 不会自动设 DeletedAt;外键约束、索引、业务逻辑(如“用户注销后冻结订单”)全得手动补。
容易踩的坑:
- 用
Preload加载关联数据时,软删的User查不到Orders—— 因为预加载也受软删除影响,默认跳过已软删记录 - 想让订单随用户软删,得在
Delete前手动更新:db.Model(&Order{}).Where("user_id = ?", userID).Update("deleted_at", time.Now()) - 事务里混合软删和真删操作时,注意
Unscoped()的作用域只对当次调用生效,别误传到关联操作里
复杂点在于:软删本质是业务状态标记,不是数据库约束。什么时候该软删、哪些关联要同步、下游系统如何感知 —— 这些都得在 Gin handler 里写清楚逻辑,GORM 不替你决定。


















