Avatar ·

Какое нулевое значение (zero value) у time.Time в Go и как проверить, пустая ли переменная времени?

Всем привет! Я только начал изучать Go и столкнулся с непонятной ситуацией при работе с типом time.Time.

У меня есть структура для хранения данных пользователя:

type User struct {
    ID        int
    Name      string
    CreatedAt time.Time
    UpdatedAt time.Time
}

Когда я создаю нового пользователя без явного указания времени, я ожидаю, что поля CreatedAt и UpdatedAt будут null или nil, как в других языках (Java, C#). Но в Go я не могу присвоить им nil — компилятор ругается.

Я попробовал так:

var u User
fmt.Println(u.CreatedAt) // Выводит: 0001-01-01 00:00:00 +0000 UTC
fmt.Println(u.CreatedAt == nil) // Ошибка компиляции: cannot convert nil to type time.Time

Вопросы:

  1. Какое значение по умолчанию (zero value) у time.Time?
  2. Почему это не nil?
  3. Как правильно проверять, было ли установлено время или оно "пустое"?
  4. Нужно ли использовать указатели *time.Time вместо time.Time для опциональных полей?

Буду благодарен за объяснение с примерами!

<<>>

Нулевое значение time.Time и его особенности

В Go каждый тип имеет нулевое значение (zero value), которое присваивается переменной при объявлении без явной инициализации. Для time.Time нулевое значение — это 1 января 0001 года, 00:00:00 UTC.

Это не nil, потому что time.Time — это структура (struct), а не указатель, интерфейс, срез, мапа или канал. Только эти типы могут быть nil. Структура всегда содержит конкретные значения всех своих полей.

Как проверить, что time.Time "пустое"?

Самый правильный способ — использовать метод IsZero():

package main

import (
    "fmt"
    "time"
)

func main() {
    var t time.Time
    fmt.Println(t.IsZero()) // true — время не установлено

    now := time.Now()
    fmt.Println(now.IsZero()) // false — время установлено
}

Когда использовать указатели *time.Time?

Если вам нужно различать "время не установлено" и "время равно 0001-01-01" (что маловероятно, но возможно), используйте указатель:

type User struct {
    ID        int
    Name      string
    CreatedAt *time.Time // nil — не установлено
    UpdatedAt *time.Time
}

func main() {
    u := User{ID: 1, Name: "Alice"}
    if u.CreatedAt == nil {
        fmt.Println("Время создания не указано")
    } else {
        fmt.Println("Создан:", *u.CreatedAt)
    }
}

Недостаток: при работе с указателями нужно всегда проверять на nil, иначе получите panic. Также усложняется сериализация/десериализация JSON.

Практические рекомендации

  • Для 95% случаев используйте time.Time и проверяйте через IsZero(). Это проще, безопаснее и производительнее.
  • Для JSON API используйте time.Time — оно корректно сериализуется в строку ISO 8601. Если нужно поле "null" в JSON, используйте указатель *time.Time или кастомный тип.
  • Для БД (например, PostgreSQL) используйте time.Time с sql.NullTime, если колонка может быть NULL.

Пример с JSON:

type Event struct {
    Name      string    `json:"name"`
    StartedAt time.Time `json:"started_at"`
    EndedAt   *time.Time `json:"ended_at,omitempty"` // nil → поле отсутствует
}

func main() {
    e := Event{Name: "Конференция", StartedAt: time.Now()}
    data, _ := json.Marshal(e)
    fmt.Println(string(data))
    // {"name":"Конференция","started_at":"2025-03-27T12:00:00Z"}
}

Вывод

Нулевое значение time.Time — это 0001-01-01. Используйте IsZero() для проверки. Указатели применяйте только когда nil принципиально отличается от нулевой даты. В большинстве случаев достаточно обычного time.Time.

<<>>

Альтернативный подход: кастомный тип для опционального времени

Если вам часто нужно различать "время не задано" и "время равно нулевой дате", можно создать свой тип-обёртку:

type OptionalTime struct {
    Time  time.Time
    Valid bool
}

func (ot OptionalTime) IsZero() bool {
    return !ot.Valid
}

// Использование:
type User struct {
    CreatedAt OptionalTime
}

u := User{}
fmt.Println(u.CreatedAt.IsZero()) // true

u.CreatedAt = OptionalTime{Time: time.Now(), Valid: true}
fmt.Println(u.CreatedAt.IsZero()) // false

Этот подход похож на sql.NullTime из стандартной библиотеки. Он удобен, когда вы работаете с базами данных и хотите явно контролировать, установлено ли значение. Однако для простых случаев time.Time с IsZero() — более идиоматичное решение.

Важно: не изобретайте велосипед без необходимости. Стандартный time.Time покрывает 99% потребностей.