用 *time.Time 表达可空时间(对应 SQL NULL)

在 Go 结构体里用 *time.Time 表示该时间字段"可能没有值"(对应 SQL NULL / JSON null),用 time.Time 表示"一定有值";决定因素是业务上有没有值,而非字段更新频率。

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

[!info] related notes

用 *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 是指针,零值为 nilnil 即”没有值”,与 SQL NULL、JSON null 一一对应:

Go 字段数据库列含义
CreatedAt time.Timecreated_at TIMESTAMP NOT NULL一定有值
LastLoginAt *time.Timelast_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":当 LastLoginAtnil 时省略该字段(或输出 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 结构体。LastLoginAtnil 表示”从未登录”,为 非 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 省略。
创建于 2026/8/9 更新于 2026/8/9