Avatar ·

Что такое нулевое значение для time.Time в Go и как проверить, пустая ли переменная времени?

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

У меня есть структура, в которой одно из полей — это дата и время. Иногда это поле может быть не задано (null из других языков). В PHP или JavaScript я бы просто проверил на null, но в Go такой роскоши нет. Я попытался использовать nil, но компилятор выдал ошибку.

Вот пример моего кода:

package main

import (
    "fmt"
    "time"
)

type Event struct {
    Name      string
    CreatedAt time.Time
    UpdatedAt time.Time // может быть не задано
}

func main() {
    e := Event{
        Name: "Тестовое событие",
        // UpdatedAt не задаю
    }
    
    // Пытаюсь проверить, задано ли UpdatedAt
    if e.UpdatedAt == nil {  // Ошибка компиляции
        fmt.Println("UpdatedAt не задано")
    }
    
    fmt.Println(e.UpdatedAt) // Выводит: 0001-01-01 00:00:00 +0000 UTC
}

Я вижу, что когда я не присваиваю значение UpdatedAt, оно выводится как 0001-01-01 00:00:00 +0000 UTC. Это и есть нулевое значение? Как мне правильно проверять, было ли поле времени задано явно или нет? И что это за дата — 0001 год?

Буду благодарен за объяснение, как работать с необязательными полями времени в Go.

<<>>

Нулевое значение time.Time и почему это не nil

В Go у каждого типа есть нулевое значение (zero value), которое присваивается переменным при объявлении без явной инициализации. Для time.Time нулевое значение — это 0001-01-01 00:00:00 +0000 UTC, что соответствует началу отсчёта времени по григорианскому календарю. Это не nil, потому что time.Time — это структура, а не указатель или интерфейс.

Почему нельзя использовать nil

nil в Go применим только к указателям, функциям, интерфейсам, срезам, каналам и мапам. time.Time — это value-тип (структура), поэтому он никогда не может быть nil. Если вам нужно «отсутствие времени», используйте указатель *time.Time.

Правильные подходы к работе с необязательным временем

1. Использование указателя на time.Time (рекомендуется)

type Event struct {
    Name      string
    CreatedAt time.Time
    UpdatedAt *time.Time // указатель — может быть nil
}

func main() {
    e := Event{
        Name: "Тестовое событие",
        // UpdatedAt не задан — будет nil
    }
    
    if e.UpdatedAt == nil {
        fmt.Println("UpdatedAt не задано")
    } else {
        fmt.Println("UpdatedAt:", *e.UpdatedAt) // разыменовываем
    }
    
    // Как задать значение:
    now := time.Now()
    e.UpdatedAt = &now
}

2. Использование метода IsZero()

Если вы по каким-то причинам не хотите использовать указатель, можно проверять, равно ли значение нулевому:

if e.UpdatedAt.IsZero() {
    fmt.Println("UpdatedAt не задано (нулевое значение)")
}

Но у этого подхода есть серьёзный недостаток: вы не сможете отличить «время не задано» от «время задано как 0001-01-01». На практике такая дата вряд ли встретится, но это всё равно потенциальная ловушка.

3. Использование отдельного флага (bool)

type Event struct {
    Name           string
    CreatedAt      time.Time
    UpdatedAt      time.Time
    UpdatedAtSet   bool
}

func main() {
    e := Event{Name: "Тест"}
    
    if !e.UpdatedAtSet {
        fmt.Println("UpdatedAt не задано")
    }
}

Этот метод громоздкий, но иногда используется в JSON-сериализации или базах данных.

Рекомендация

  • Для полей, которые могут отсутствовать, используйте *time.Time — это идиоматично для Go.
  • При работе с JSON используйте тег omitempty: UpdatedAt *time.Time \`json:"updated_at,omitempty"\` — тогда нулевые значения не будут сериализоваться.
  • В базах данных (например, PostgreSQL) для nullable-колонок используйте тип *time.Time или библиотеки типа sql.NullTime.
  • Не пытайтесь сравнивать time.Time с nil — это вызовет ошибку компиляции.

Почему именно 0001 год?

Go использует пролептический григорианский календарь. Нулевое значение time.Time соответствует полуночи 1 января 0001 года по UTC. Это минимально возможное значение для типа time.Time.

<<>>

Альтернативный подход: использование NullTime из пакета database/sql

Если вы работаете с базами данных, обратите внимание на встроенный тип sql.NullTime из пакета database/sql. Он специально создан для представления nullable-полей времени:

import "database/sql"

type Event struct {
    Name      string
    CreatedAt time.Time
    UpdatedAt sql.NullTime
}

func main() {
    e := Event{Name: "Тест"}
    
    if !e.UpdatedAt.Valid {
        fmt.Println("UpdatedAt не задано")
    } else {
        fmt.Println("UpdatedAt:", e.UpdatedAt.Time)
    }
    
    // Как задать значение:
    e.UpdatedAt = sql.NullTime{
        Time:  time.Now(),
        Valid: true,
    }
}

sql.NullTime содержит два поля: Time time.Time и Valid bool. Это удобно при сканировании строк из БД — если в колонке NULL, то Valid будет false.

Когда использовать

  • При прямом взаимодействии с SQL-базами данных через database/sql
  • Если вам не хочется возиться с указателями и разыменованием
  • Когда нужна явная проверка на валидность значения

Минус: sql.NullTime не поддерживает JSON-сериализацию «из коробки» — придётся писать кастомные методы MarshalJSON/UnmarshalJSON. Для JSON лучше использовать *time.Time.