用 *time.Time 更稳妥。它能区分未删除(nil)和已删除(含时间戳),兼容 GORM 软删除机制;若用 bool 或非指针 time.Time 易引发误判、逻辑遗漏及关联不一致问题。

软删除字段该用 bool 还是 time.Time?
用 time.Time 更稳妥。虽然 bool 看似简单,但无法区分“未删除”和“删除时间未知”,也丢失了可审计的删除时间戳。生产环境建议统一用 deleted_at *time.Time(指针类型),这样能明确表达“未删除 = nil”,且与 GORM 的软删除机制原生兼容。如果用 bool deleted,就得自己重写查询逻辑、Scope 和恢复逻辑,容易漏掉关联查询场景。
常见错误是定义成 deleted_at time.Time(非指针),导致零值 0001-01-01 00:00:00 +0000 UTC 被误判为已删除。务必用指针:
type User struct {
ID uint `gorm:"primaryKey"`
Name string
DeletedAt *time.Time `gorm:"index"`
}
GORM 默认软删除行为怎么关掉?
GORM v2 会自动识别 DeletedAt 字段并启用软删除——所有 Find、First、Where 查询默认跳过 DeletedAt IS NOT NULL 的记录。不需要额外配置,但必须注意:这个行为只对字段名严格匹配 DeletedAt(大小写敏感)且类型为 *time.Time 生效。
如果你用的是自定义字段名(比如 is_deleted),GORM 不会自动处理,得手动加条件:
db.Where("is_deleted = ?", false).Find(&users)
或者用 Unscoped() 绕过默认过滤,但别滥用——它会跳过所有软删除检查,包括恢复时的幂等性校验。
恢复数据时为什么 Unscoped().Save() 不生效?
因为 Save() 不会更新 DeletedAt 字段(GORM 认为它是“受控字段”)。直接赋值 user.DeletedAt = nil 后调用 Save(),GORM 默认忽略该字段变更。
正确做法只有两种:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用
Unscoped().Model(&user).Update("deleted_at", nil)—— 显式指定要更新的字段 - 或更推荐:用 GORM 内置的
Unscoped().Restore()(需确保结构体含DeletedAt字段)
关联数据的软删除怎么保持一致性?
GORM 不会自动级联软删除。比如删除一个 Order,它的 OrderItems 不会自动被标记删除。你得手动处理:
- 在业务层显式调用:
db.Model(&order).Association("Items").Find(&items); db.Unscoped().Model(&Item{}).Where("id IN ?", itemIDs).Update("deleted_at", time.Now()) - 或用 GORM 的
BeforeDelete钩子,在Order软删除前批量更新关联项 - 硬删除场景下才考虑数据库外键
ON DELETE CASCADE,但软删除必须靠代码控制
DeletedAt 做显式判断或交由 GORM 统一拦截;恢复操作尤其要防重复、防遗漏、防关联断裂。


















