Нулевое значение time.Time в Go: ловушки, которые убивают ваш код

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Тот самый баг, который ловил каждого

Вы когда-нибудь получали в логах дату 0001-01-01 00:00:00 +0000 UTC и думали: «Откуда это взялось? Я же ничего не инициализировал!»? Поздравляю, вы встретились с нулевым значением time.Time в Go. Это классическая ловушка, которая собирает тысячи просмотров на Stack Overflow и сотни часов дебага по всему миру.

В этой статье мы разберем, что такое zero value для time.Time, почему оно ведет себя неожиданно, как отличить «пустую» дату от реальной и, главное, — как не наступить на эти грабли в продакшене. Если вы пишете на Go и работаете с датами, этот материал спасет вас от нервного тика.

Что такое zero value в Go и почему time.Time — особенный случай

В Go у каждого типа есть нулевое значение (zero value). Для чисел это 0, для строк — "", для указателей — nil. Логика простая: если вы объявили переменную без инициализации, она получает «пустышку». Но с time.Time всё сложнее.

Нулевое значение time.Time — это не Unix timestamp 0 (1970-01-01), как можно было бы ожидать. Это 0001-01-01 00:00:00 +0000 UTC. Почему? Потому что внутренне time.Time хранит наносекунды с 1 января 1 года (по григорианскому календарю). Ноль в этом представлении и дает год 1.

package main

import (
    "fmt"
    "time"
)

func main() {
    var t time.Time
    fmt.Println(t) // 0001-01-01 00:00:00 +0000 UTC
    fmt.Println(t.Unix()) // -62135596800 (отрицательное число!)
}

Уже видите проблему? Если вы попытаетесь проверить if t == 0 или сравнить с Unix-нулем, ваш код сломается. Давайте разберем топ ошибок с zero value time.Time и способы их избежать.

Топ-5 ошибок с нулевым значением time.Time

1. Сравнение с Unix timestamp 0

Самая распространенная ошибка — попытка проверить, что время «пустое», сравнив его с time.Unix(0, 0). Это не сработает, потому что нулевое значение — это не 1970 год.

// ❌ Ошибка: никогда не будет true
var t time.Time
if t == time.Unix(0, 0) {
    fmt.Println("Время пустое")
}

// ✅ Правильно: используем метод IsZero()
if t.IsZero() {
    fmt.Println("Время пустое")
}

Метод IsZero() — ваш главный инструмент. Он возвращает true только для нулевого значения, а не для любой «ранней» даты.

2. Сериализация в JSON: неожиданный null

Когда вы сериализуете структуру с time.Time в JSON, нулевое значение превращается в строку "0001-01-01T00:00:00Z". Это ломает API, фронтенд и базы данных. Клиенты видят странную дату, а не null.

type Event struct {
    Name      string    `json:"name"`
    StartTime time.Time `json:"start_time"`
}

func main() {
    e := Event{Name: "test"}
    data, _ := json.Marshal(e)
    fmt.Println(string(data))
    // {"name":"test","start_time":"0001-01-01T00:00:00Z"}
    // ❌ Должно быть null или поле должно отсутствовать
}

Решение: используйте указатель на time.Time (*time.Time) или кастомный тип с логикой сериализации.

// ✅ Вариант с указателем
type Event struct {
    Name      string     `json:"name"`
    StartTime *time.Time `json:"start_time,omitempty"`
}

// ✅ Или кастомный тип с omitempty
type CustomTime struct {
    time.Time
}

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    if ct.IsZero() {
        return []byte("null"), nil
    }
    return json.Marshal(ct.Time)
}

3. Проблемы с базами данных (SQL)

При вставке нулевого time.Time в PostgreSQL или MySQL вы получите дату 0001-01-01, что вызовет ошибку ограничения диапазона (год 1 вне допустимого диапазона для некоторых СУБД).

// ❌ Ошибка: year out of range
db.Exec("INSERT INTO events (start_time) VALUES ($1)", time.Time{})

// ✅ Правильно: используем sql.NullTime
var t sql.NullTime
if !t.Valid {
    // Вставляем NULL
    db.Exec("INSERT INTO events (start_time) VALUES (NULL)")
}

Всегда используйте sql.NullTime или указатели для nullable-полей даты в БД.

4. Сравнение с временем в прошлом

Другая ловушка — проверка «если время не установлено, то оно в прошлом». Нулевое значение — это 1 год н.э., что «меньше» любой реальной даты, поэтому условие t.Before(time.Now()) вернет true для пустого времени. Это приводит к ложным срабатываниям.

// ❌ Ошибка: пустое время пройдет проверку
if t.Before(time.Now()) {
    fmt.Println("Событие уже прошло") // true для нулевого t!
}

// ✅ Правильно: сначала проверяем IsZero()
if !t.IsZero() && t.Before(time.Now()) {
    fmt.Println("Событие уже прошло")
}

5. Использование time.Time как ключа в map

Нулевое значение time.Time может случайно перезаписать другие записи в map, если вы используете его как ключ. Это редкий, но коварный баг.

cache := make(map[time.Time]string)
cache[time.Time{}] = "default"
cache[someRealTime] = "real"

// Потом вы ищете по нулевому значению — получаете "default"
// Но если забудете проверить IsZero(), логика сломается

Лучше не использовать time.Time как ключ map, а заменить на Unix-метку (int64) или строку.

Как правильно работать с zero value time.Time: лучшие практики

Теперь, когда мы разобрали ошибки, давайте составим чек-лист для Go-разработчика:

  • Всегда используйте IsZero() для проверки, установлено ли время. Никогда не сравнивайте с time.Unix(0, 0) или константами.
  • Для JSON и API используйте *time.Time с тегом omitempty или кастомный тип. Клиенты должны видеть null, а не 1 год н.э.
  • <

Блоги

Рекомендаций по книгам