实战:从 User 结构体看 time.Time 与 *time.Time 的选择

2026-08-09 阅读个人项目 User 模型时,注意到 CreatedAt 用 time.Time、LastLoginAt 用 *time.Time,经 AI 指导厘清"指针是为了表达可能没有值(SQL NULL),而非因为频繁修改",并双向链接到 nil 形态、GORM、struct 标签等理论笔记。

#type / journal #status / growing #tech / dev #resource / go

[!info] related notes

实战:从 User 结构体看 time.Time 与 *time.Time 的选择

时间与上下文

2026-08-09,在个人项目的真实代码里读 User 模型定义时,注意到两个时间字段的写法不一样:

type User struct {
    ID           uuid.UUID  `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()" json:"id"`
    Email        string     `gorm:"type:varchar(255);uniqueIndex;not null" json:"email"`
    PasswordHash string     `gorm:"type:varchar(255);not null" json:"-"`
    CreatedAt    time.Time  `gorm:"not null;default:now()" json:"created_at"`
    LastLoginAt  *time.Time `json:"last_login_at,omitempty"`
}

发生了什么 / 观察

CreatedAttime.TimeLastLoginAt*time.Time。第一反应是:是不是因为 LastLoginAt 会频繁更新,所以才用指针?带着这个疑问去问了 AI。

观察、决定 / 结论

结论(已沉淀为 用 *time.Time 表达可空时间):

  • 判断标准只有一个:这个字段在业务上”有没有可能没有值”,而不是”改不改”。
  • CreatedAt 用值类型:用户一旦创建,创建时间必然存在(NOT NULL)。
  • LastLoginAt 用指针:刚注册、从未登录的用户没有”最后登录时间”,nil 正好对应 SQL NULL;若强行用值类型,零值 0001-01-01 会与”真有这么个时间”无法区分。
  • 序列化侧 omitemptynil 时省略字段,避免把零值时间泄漏给前端。
  • 关键反直觉点:更新频率与是否用指针正交——“*time.Time 是因为频繁修改”是个误区。

对应理论笔记的具体落点:

  • nil 的多种形态 §12:指针字段在 JSON / 协议层编码为 null 或经 omitempty 省略,正是这里 LastLoginAt 的机制来源。
  • GORM:模型映射与零值更新边界,*time.Time 对应允许 NULL 的列。
  • Go struct 标签gorm:"not null;default:now()"json:"...omitempty" 怎么驱动映射与序列化。
  • database/sql:原生场景的备选 sql.NullTime

下一步动作

  • 继续在实战代码里收集”一个代码细节 ↔ 一条理论笔记”的对照,追加到 Go 语言项目实战 MOC
  • 把本项目里其他”可空字段”(如 DeletedAtVerifiedAt)也按同一原则核对一遍。
创建于 2026/8/9 更新于 2026/8/9