结论:别盲目嵌入gorm.Model,因其ID/CreatedAt/UpdatedAt/DeletedAt四字段易致冗余、精度错配与软删除误用;适合快速原型,但真实业务需权衡软删除必要性、时间类型(如int64毫秒)及主键类型(如int64/string),推荐按需自定义Base结构体。

直接说结论:别盲目嵌入 gorm.Model,它自带的四个字段(ID, CreatedAt, UpdatedAt, DeletedAt)不是万能模板,反而常导致字段冗余、时间精度错配、软删除误用等问题。
什么时候该用 gorm.Model,什么时候该手动定义
嵌入 gorm.Model 适合快速原型或 CRUD 密集型小项目,但真实业务中往往要权衡三点:
- 是否真需要软删除?
DeletedAt会强制加索引,且所有查询默认忽略已“删除”记录——如果你用的是硬删或逻辑状态字段(如status),它就是累赘 - 时间字段是否必须是
time.Time?很多服务要求毫秒级 UNIX 时间戳(int64),而gorm.Model固定为time.Time,改起来反而要覆盖全部字段标签 - 主键是否一定是
uint?金融类系统常用int64或string(雪花 ID),gorm.Model.ID的类型和标签无法复用
更稳妥的做法是只复用你需要的部分,比如:
type Base struct {
ID uint64 `gorm:"primaryKey"`
CreatedAt int64 `gorm:"autoCreateTime:milli"`
UpdatedAt int64 `gorm:"autoUpdateTime:milli"`
}
type User struct {
Base
Name string
}
CreatedAt 和 UpdatedAt 的时间精度陷阱
GORM 默认用 time.Time 类型填充这两个字段,但底层数据库时间精度可能不一致。MySQL 5.6 默认只支持秒级,而 time.Now() 带纳秒——结果就是插入时被截断,多次更新后 UpdatedAt 看似没变。
- 明确指定精度:用
autoCreateTime:milli或autoUpdateTime:nano标签,配合对应类型(int64存毫秒,int64存纳秒) - 避免混用:不要在一个结构体里同时写
CreatedAt time.Time和UpdatedAt int64,GORM 不保证两者更新时机一致 - 注意时区:
parseTime=True必须开启,否则time.Time字段从 MySQL 读出来是字符串,容易 panic
Model 方法调用时,结构体嵌入不影响表名推导
很多人以为 db.Model(&User{}).Find(&result) 中的 &User{} 会影响字段选择,其实只决定两件事:
- 表名:GORM 按结构体名复数推导(
User→users),和是否嵌入gorm.Model无关 - 默认字段范围:仅当
Find的目标变量(如&APIUser{})字段少于Model结构体时,才按目标结构体字段生成SELECT列表
换句话说:db.Model(&User{}).Find(&APIUser{}) 查的是 users 表,但只取 APIUser 里定义的字段——嵌入的 gorm.Model 字段(如 DeletedAt)根本不会出现在 SQL 里,除非你显式 Select("id, name, deleted_at")。
软删除字段 DeletedAt 的实际约束力很弱
DeletedAt 被标记为 gorm.DeletedAt 并带 index 标签,但它只是个普通指针字段,GORM 不强制你用 Delete 方法来设值。
- 手动赋值
u.DeletedAt = &now不会触发软删除逻辑,得用db.Unscoped().Delete(&u)才绕过,而db.Delete(&u)才真正软删 - 如果业务已有状态字段(如
status tinyint),强行加DeletedAt会导致双重逻辑,查数据时容易漏条件 - 迁移旧表时,
DeletedAt默认值是NULL,但 GORM 的First/Find默认加WHERE deleted_at IS NULL,老数据全查不到——得先跑 SQL 把deleted_at设为NULL,否则要加Unscoped()
真正关键的不是字段是否存在,而是你是否统一使用 GORM 的软删除语义。一旦混合手写 SQL、原生驱动操作或跨服务共享表,DeletedAt 就变成一个容易被忽略的隐性约定。


















