实战:从 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
- 所属系列:Go 语言项目实战 MOC
- 沉淀为概念:用 *time.Time 表达可空时间
- 理论笔记:nil 的多种形态、GORM、Go struct 标签、database/sql、JSON 与序列化
实战:从 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"`
}
发生了什么 / 观察
CreatedAt 是 time.Time,LastLoginAt 是 *time.Time。第一反应是:是不是因为 LastLoginAt 会频繁更新,所以才用指针?带着这个疑问去问了 AI。
观察、决定 / 结论
结论(已沉淀为 用 *time.Time 表达可空时间):
- 判断标准只有一个:这个字段在业务上”有没有可能没有值”,而不是”改不改”。
CreatedAt用值类型:用户一旦创建,创建时间必然存在(NOT NULL)。LastLoginAt用指针:刚注册、从未登录的用户没有”最后登录时间”,nil正好对应 SQLNULL;若强行用值类型,零值0001-01-01会与”真有这么个时间”无法区分。- 序列化侧
omitempty让nil时省略字段,避免把零值时间泄漏给前端。 - 关键反直觉点:更新频率与是否用指针正交——“
*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。
- 把本项目里其他”可空字段”(如
DeletedAt、VerifiedAt)也按同一原则核对一遍。