用 *time.Time 表达可空时间(对应 SQL NULL)
在 Go 结构体里用 *time.Time 表示该时间字段"可能没有值"(对应 SQL NULL / JSON null),用 time.Time 表示"一定有值";决定因素是业务上有没有值,而非字段更新频率。
[!info] related notes
- 所属 MOC:Go 语言项目实战 · Go 数据访问 MOC
- 前置概念:nil 的多种形态、Go 基本类型
- 相关:GORM、Go struct 标签、database/sql、JSON 与序列化
- 实战:User 结构体时间字段实战
- 易混淆:更新频率 ≠ 是否可空
用 *time.Time 表达可空时间(对应 SQL NULL)
一句话定义
在 Go 结构体里,用 *time.Time 表示”这个时间字段可能没有值”,它天然对应数据库列的 NULL 与 JSON 的 null;用 time.Time(值类型)表示”一定有值”,对应 NOT NULL。 选哪一种的唯一判断标准是业务上”这个值有没有可能不存在”,而不是”这个值会不会频繁改变”。
核心机制 / 工作原理
1. time.Time 是值类型,零值无法表达”无”
time.Time 是结构体值类型,声明不初始化时的零值是:
time.Time{} // 0001-01-01 00:00:00 +0000 UTC
它必定有一个具体值。当数据库里该列为 NULL 时,Go 仍然必须填一个值,于是零值被误当成”公元 1 年登录过”——语义崩坏,无法区分”从未登录”与”真有这么个时间”。
2. *time.Time 的 nil 正好对应 NULL
*time.Time 是指针,零值为 nil。nil 即”没有值”,与 SQL NULL、JSON null 一一对应:
| Go 字段 | 数据库列 | 含义 |
|---|---|---|
CreatedAt time.Time | created_at TIMESTAMP NOT NULL | 一定有值 |
LastLoginAt *time.Time | last_login_at TIMESTAMP NULL | 可能没有值(如刚注册从未登录) |
3. 判断标准:有/没有值,而非改不改
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用值类型:用户一旦被创建,创建时间必然存在,不存在”已存在但不知何时创建”的正常情况。LastLoginAt用指针:刚注册、从未登录的用户没有”最后登录时间”,nil= 未登录,非 nil= 有真实时间。- 误区:“
*time.Time是因为这个字段会频繁更新?“——不是。更新频率与是否用指针正交:CreatedAt几乎不改却用值类型,LastLoginAt常改却用指针,二者都只看”有没有值”(更完整的推导见 User 结构体实战)。
4. 序列化侧
json:"last_login_at,omitempty":当 LastLoginAt 为 nil 时省略该字段(或输出 null),避免把零值时间 0001-01-01 泄漏给前端。
GORM 侧:*time.Time 字段对应的列默认允许 NULL;值类型字段默认 NOT NULL(除非显式 not null 约束或零值被忽略——见 GORM 零值更新 的边界)。
5. 备选方案
原生 database/sql 可用 sql.NullTime(带 Valid bool 字段)显式区分”有值且有效”与”无值”;在 GORM 模型里更惯用 *time.Time,映射更直观,ORM 直接按指针判 NULL。
最小例子 / 最小场景
见上方 User 结构体。LastLoginAt 为 nil 表示”从未登录”,为 非 nil 表示真实时间;CreatedAt 永远非 nil。
边界与易混淆点
- 不要为”可空”而给一个永远有值的字段加指针:徒增判空负担,且 JSON 序列化会多一层
null/省略处理。 - 不要用
time.Time零值当哨兵表示”无”:0001-01-01与真实时间无法区分。 - 指针判空:
if u.LastLoginAt != nil { t := *u.LastLoginAt },或配合time.Time.IsZero()作为辅助判断(仅当你确实拿到值类型时)。 - 与 nil 的多种形态 的关系:
*time.Time只是”指针作可空容器”的一个具体实例;nil 指针在 JSON / 协议层会被编码为null或经omitempty省略。