Введение: Тот самый баг, который ловил каждого
Вы когда-нибудь получали в логах дату 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 год н.э.
<