🐹 Go

Trabalhando com data e hora em Go (Golang): um guia detalhado

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — директор школы. У вас есть расписание звонков, даты каникул, дни рождения учеников и сроки сдачи отчетов. Без часов и календаря в школе наступил бы хаос: уроки начинались бы когда вздумается, а каникулы — зимой вместо лета.

В программировании то же самое. Работа с датой и временем — это умение программы понимать, когда что-то происходит: сейчас, вчера, через неделю или 1 января 2025 года в 15:30. Без этого не работает ни один современный сервис:

  • 📅 Бронирование отелей — нужно знать, свободен ли номер 12 августа
  • ⏰ Будильники и напоминания — разбудить в 7 утра
  • 📊 Аналитика — сколько пользователей зашли вчера
  • 💳 Банки — когда придет зарплата и не просрочен ли платеж

В Go (Golang) работа с датой и временем — это встроенная возможность. Вам не нужно скачивать дополнительные библиотеки: всё уже есть в пакете time. Это как если бы в каждой школьной парте уже были встроены часы и календарь — осталось только научиться ими пользоваться.

💡 Важно: В Go нет отдельного типа «дата». Есть только момент времени (time.Time), который включает и дату, и время. Нельзя сказать «15 марта» — нужно сказать «15 марта 2025 года 00:00:00». Это особенность языка, к которой быстро привыкаешь.

Теория: как это работает изнутри

Чтобы понять, как Go работает со временем, давайте заглянем «под капот». Компьютер не понимает «понедельник» или «июнь». Он понимает только числа. Поэтому все даты и время в программировании хранятся как одно большое число.

Unix Time — «всемирное время»

В Go (как и в большинстве языков) время хранится в формате Unix time — это количество секунд, прошедших с 1 января 1970 года 00:00:00 UTC. Почему именно эта дата? Потому что в 1970 году появилась операционная система Unix, и разработчики договорились считать время от этого момента.

Например, число 1 704 000 000 — это 1 января 2024 года в 00:00:00 по UTC. Go хранит это число внутри структуры time.Time и умеет преобразовывать его в понятный человеку вид: год, месяц, день, час, минута, секунда.

Тип time.Time — главный герой

В Go время представлено структурой time.Time. Она содержит:

  • wall clock — настенные часы (текущее время)
  • monotonic clock — монотонные часы (для измерения интервалов, не сбрасываются при переводе часов)
  • location — часовой пояс

Это сделано для точности: если вы замеряете, сколько выполняется функция, вам не важно, что кто-то перевел часы на зимнее время. Монотонные часы всегда идут вперед.

Часовые пояса — головная боль всего мира

Земля разделена на 24 часовых пояса. Когда в Москве полдень, в Нью-Йорке — 4 утра, а в Токио — 6 вечера. Go хранит время в UTC (всемирное координированное время), а при выводе преобразует в нужный часовой пояс.

⚠️ Золотое правило: В базе данных и в логике программы всегда храните время в UTC. Преобразуйте в локальное время только при показе пользователю. Это спасет вас от бесконечных ошибок с часовыми поясами.

Тип Duration — промежуток времени

Кроме момента времени, Go умеет работать с длительностью — типом time.Duration. Это просто количество наносекунд (1 секунда = 1 000 000 000 наносекунд).

Вы можете сказать: «добавь 2 часа 30 минут к текущему времени». Go переведет это в наносекунды и выполнит операцию.

Монотонные часы — почему это важно

Представьте, что вы засекаете время выполнения функции. Если в этот момент Windows переведет часы на час назад (из-за перехода на летнее время), то обычные часы покажут, что функция выполнялась -50 минут. Абсурд! Монотонные часы не зависят от перевода стрелок — они всегда увеличиваются.

В Go при вызове time.Now() вы получаете оба значения. При сравнении и вычислении разницы используются монотонные часы, если они доступны.

Синтаксис в Go (Golang): все формы и варианты

Пакет time — это стандартная библиотека Go. Его не нужно устанавливать, только импортировать:

import "time"

Основные функции и методы

Функция/Метод Описание Пример
time.Now() Текущий момент времени now := time.Now()
time.Date() Создать дату из компонентов t := time.Date(2025, 3, 15, 10, 30, 0, 0, time.UTC)
t.Format() Преобразовать время в строку t.Format("2006-01-02")
time.Parse() Преобразовать строку во время t, _ := time.Parse("2006-01-02", "2025-03-15")
t.Add() Прибавить длительность t.Add(2 * time.Hour)
t.Sub() Разница между двумя моментами diff := t2.Sub(t1)
t.Before() Проверить, раньше ли t1.Before(t2)
t.After() Проверить, позже ли t1.After(t2)
t.Equal() Проверить равенство t1.Equal(t2)
time.Since() Время, прошедшее с момента time.Since(start)
time.Until() Время до момента time.Until(deadline)
time.Sleep() Приостановить выполнение time.Sleep(1 * time.Second)
t.In() Сменить часовой пояс t.In(time.FixedZone("MSK", 3*60*60))
t.Unix() Получить Unix timestamp t.Unix()

Формат времени — «магическое число» 2006-01-02

В Go используется уникальный способ форматирования даты. Вместо символов типа YYYY-MM-DD используется конкретная дата:

Mon Jan 2 15:04:05 MST 2006

Это 2 января 2006 года, 15:04:05, понедельник, часовой пояс MST. Каждая часть этой даты — шаблон:

Шаблон Значение Пример вывода
2006 Год (4 цифры) 2025
06 Год (2 цифры) 25
01 Месяц (2 цифры) 03
1 Месяц (1-2 цифры) 3
Jan Месяц (сокращенный) Mar
January Месяц (полный) March
02 День (2 цифры) 15
2 День (1-2 цифры) 15
Mon День недели (сокращенный) Sat
Monday День недели (полный) Saturday
15 Час (24-часовой) 10
3 Час (12-часовой) 10
04 Минуты 30
05 Секунды 00
PM AM/PM AM
MST Часовой пояс UTC
-0700 Смещение часового пояса +0300
🔥 Запомните: 2006-01-02 15:04:05 — это не случайные числа. Это 1:04:05 после полудня 2 января 2006 года. Выучите эту дату, и форматирование в Go станет для вас родным.

Константы длительности

Для удобства в Go определены константы:

time.Nanosecond  // 1 наносекунда
time.Microsecond // 1000 наносекунд
time.Millisecond // 1000 микросекунд
time.Second      // 1000 миллисекунд
time.Minute      // 60 секунд
time.Hour        // 60 минут

Их можно комбинировать: 2*time.Hour + 30*time.Minute + 15*time.Second

Примеры кода — от простого к сложному

Пример 1: Текущее время и его компоненты

package main

import (
    "fmt"
    "time"
)

func main() {
    // Получаем текущий момент времени
    now := time.Now()
    
    // Выводим целиком
    fmt.Println("Сейчас:", now)
    
    // Получаем отдельные компоненты
    year := now.Year()       // год
    month := now.Month()     // месяц (тип time.Month)
    day := now.Day()         // день месяца
    hour := now.Hour()       // часы
    minute := now.Minute()   // минуты
    second := now.Second()   // секунды
    weekday := now.Weekday() // день недели
    
    fmt.Printf("Сегодня: %d %s %d года
", day, month, year)
    fmt.Printf("Точное время: %02d:%02d:%02d
", hour, minute, second)
    fmt.Printf("День недели: %s
", weekday)
    
    // Unix timestamp — количество секунд с 1970 года
    unixTime := now.Unix()
    fmt.Printf("Unix time: %d
", unixTime)
}

Объяснение: Мы получаем текущее время через time.Now(). Затем извлекаем из него год, месяц, день, часы, минуты, секунды и день недели. Обратите внимание на %02d — это формат вывода с ведущим нулем (чтобы было 09, а не 9). В конце получаем Unix timestamp — число, понятное компьютеру.

Пример 2: Создание конкретной даты

package main

import (
    "fmt"
    "time"
)

func main() {
    // Создаем дату: 15 марта 2025 года, 10:30:00, UTC
    // Параметры: год, месяц, день, час, мин, сек, наносек, часовой пояс
    meeting := time.Date(2025, time.March, 15, 10, 30, 0, 0, time.UTC)
    
    fmt.Println("Встреча назначена на:", meeting)
    
    // Можно использовать число вместо time.Month
    birthday := time.Date(2000, 7, 20, 0, 0, 0, 0, time.UTC)
    fmt.Println("День рождения:", birthday)
    
    // Сравниваем даты
    now := time.Now()
    if meeting.After(now) {
        fmt.Println("Встреча еще в будущем")
    } else if meeting.Before(now) {
        fmt.Println("Встреча уже прошла")
    } else {
        fmt.Println("Встреча прямо сейчас!")
    }
}

Объяснение: Функция time.Date() принимает 8 параметров. Месяц можно указать как константу (time.March) или как число (3). Часовой пояс time.UTC — это всемирное время. Методы After() и Before() возвращают true или false, позволяя сравнивать даты.

Пример 3: Форматирование даты в строку

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()
    
    // Стандартные форматы
    fmt.Println("RFC3339:", now.Format(time.RFC3339))
    fmt.Println("ANSIC:", now.Format(time.ANSIC))
    fmt.Println("Kitchen:", now.Format(time.Kitchen))
    
    // Пользовательские форматы (используем магическую дату 2006-01-02)
    fmt.Println("YYYY-MM-DD:", now.Format("2006-01-02"))
    fmt.Println("DD.MM.YYYY:", now.Format("02.01.2006"))
    fmt.Println("ЧЧ:ММ:СС:", now.Format("15:04:05"))
    fmt.Println("Полная дата:", now.Format("02 January 2006, Monday"))
    fmt.Println("Для файла:", now.Format("2006-01-02_15-04-05"))
    
    // 12-часовой формат с AM/PM
    fmt.Println("12-часовой:", now.Format("03:04 PM"))
}

Объяснение: Go использует «магическую дату» 2006-01-02 для форматирования. time.RFC3339 — это встроенный формат для обмена данными (JSON, API). Обратите внимание: в пользовательском формате мы пишем именно 2006, 01, 02, 15, 04, 05 — это не текущие значения, а шаблоны.

Пример 4: Разбор строки в дату (парсинг)

package main

import (
    "fmt"
    "time"
)

func main() {
    // Парсим дату из строки
    // Первый аргумент — шаблон (как выглядит строка)
    // Второй — сама строка
    dateStr := "2025-03-15"
    parsed, err := time.Parse("2006-01-02", dateStr)
    if err != nil {
        fmt.Println("Ошибка парсинга:", err)
        return
    }
    fmt.Println("Распарсено:", parsed)
    
    // Парсим с временем
    datetimeStr := "15.03.2025 14:30:00"
    parsed2, _ := time.Parse("02.01.2006 15:04:05", datetimeStr)
    fmt.Println("С временем:", parsed2)
    
    // Парсим с часовым поясом
    withTZ := "2025-03-15T10:30:00+03:00"
    parsed3, _ := time.Parse(time.RFC3339, withTZ)
    fmt.Println("С часовым поясом:", parsed3)
    
    // Ошибка: несовпадение формата
    wrong := "2025/03/15"
    _, err = time.Parse("2006-01-02", wrong)
    if err != nil {
        fmt.Println("Ожидаемая ошибка:", err)
    }
}

Объяснение: time.Parse() — обратная операция к Format(). Шаблон должен точно соответствовать формату строки. Если строка "15.03.2025", то и шаблон должен быть "02.01.2006" с точками. При ошибке возвращается error — всегда проверяйте его!

Пример 5: Работа с интервалами времени

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    
    // Имитируем какую-то работу
    time.Sleep(2 * time.Second)
    
    end := time.Now()
    
    // Разница между двумя моментами
    duration := end.Sub(start)
    fmt.Println("Прошло:", duration)
    
    // Более удобный способ
    elapsed := time.Since(start)
    fmt.Println("Прошло (since):", elapsed)
    
    // Сколько осталось до дедлайна
    deadline := time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC)
    remaining := time.Until(deadline)
    fmt.Println("До Нового года осталось:", remaining)
    
    // Прибавляем время
    now := time.Now()
    later := now.Add(3 * time.Hour)        // через 3 часа
    tomorrow := now.Add(24 * time.Hour)     // завтра
    nextWeek := now.Add(7 * 24 * time.Hour) // через неделю
    
    fmt.Println("Через 3 часа:", later)
    fmt.Println("Завтра:", tomorrow)
    fmt.Println("Через неделю:", nextWeek)
    
    // Вычитаем время
    yesterday := now.Add(-24 * time.Hour)
    fmt.Println("Вчера:", yesterday)
}

Объяснение: time.Since() и time.Until() — удобные обертки для Sub(). Add() принимает time.Duration (положительное или отрицательное). Обратите внимание: time.Sleep() приостанавливает программу на заданное время — полезно для имитации работы.

Пример 6: Часовые пояса

package main

import (
    "fmt"
    "time"
)

func main() {
    // Текущее время в UTC
    now := time.Now().UTC()
    fmt.Println("UTC:", now)
    
    // Загружаем часовой пояс
    msk, err := time.LoadLocation("Europe/Moscow")
    if err != nil {
        fmt.Println("Не удалось загрузить часовой пояс:", err)
        return
    }
    
    // Преобразуем в московское время
    moscowTime := now.In(msk)
    fmt.Println("Москва:", moscowTime)
    
    // Нью-Йорк
    ny, _ := time.LoadLocation("America/New_York")
    nyTime := now.In(ny)
    fmt.Println("Нью-Йорк:", nyTime)
    
    // Токио
    tokyo, _ := time.LoadLocation("Asia/Tokyo")
    tokyoTime := now.In(tokyo)
    fmt.Println("Токио:", tokyoTime)
    
    // Создаем свой часовой пояс (UTC+5)
    ekb := time.FixedZone("Екатеринбург", 5*60*60)
    ekbTime := now.In(ekb)
    fmt.Println("Екатеринбург:", ekbTime)
    
    // Сравнение: разные пояса, но один момент времени
    fmt.Println("Равны ли?", moscowTime.Equal(nyTime)) // true — это один и тот же момент!
}

Объяснение: time.LoadLocation() загружает часовой пояс из базы данных IANA (встроена в Go). time.FixedZone() создает свой пояс с фиксированным смещением. Важно: Equal() сравнивает моменты времени, а не строки. 12:00 MSK и 04:00 EST — это одно и то же время.

Пример 7: Таймеры и тикеры

package main

import (
    "fmt"
    "time"
)

func main() {
    // Таймер — срабатывает один раз через заданное время
    fmt.Println("Запускаем таймер на 2 секунды...")
    timer := time.NewTimer(2 * time.Second)
    
    // Ждем сигнала от таймера
    <-timer.C
    fmt.Println("Таймер сработал!")
    
    // Тикер — срабатывает периодически
    fmt.Println("
Запускаем тикер (каждую секунду, 5 раз):")
    ticker := time.NewTicker(1 * time.Second)
    
    // Создаем свой таймер для остановки
    stopTimer := time.NewTimer(5 * time.Second)
    
    // Бесконечный цикл, но мы выйдем через 5 секунд
    for {
        select {
        case t := <-ticker.C:
            fmt.Println("Тик:", t.Format("15:04:05"))
        case <-stopTimer.C:
            fmt.Println("Стоп! Прошло 5 секунд.")
            ticker.Stop() // Останавливаем тикер
            return
        }
    }
}

Объяснение: time.NewTimer() создает канал, который пришлет сигнал через указанное время. time.NewTicker() создает канал, который присылает сигналы через равные промежутки. select — это как switch, но для каналов: выполняет тот case, который готов первым. Тикер нужно обязательно останавливать через Stop(), чтобы не было утечки памяти.

Пример 8: Измерение времени выполнения функции

package main

import (
    "fmt"
    "time"
)

// Функция, которую будем измерять
func hardWork() {
    // Имитируем сложные вычисления
    sum := 0
    for i := 0; i < 1000000; i++ {
        sum += i
    }
    time.Sleep(500 * time.Millisecond) // Имитация задержки
    fmt.Println("Результат:", sum)
}

func main() {
    // Способ 1: вручную
    start := time.Now()
    hardWork()
    elapsed := time.Since(start)
    fmt.Printf("Функция выполнялась: %v
", elapsed)
    
    // Способ 2: с помощью defer (выполняется при выходе из функции)
    measure := func() {
        start := time.Now()
        hardWork()
        fmt.Printf("Измерение через defer: %v
", time.Since(start))
    }
    measure()
    
    // Способ 3: точность до наносекунд
    start = time.Now()
    hardWork()
    fmt.Printf("Наносекунд: %d
", time.Since(start).Nanoseconds())
    fmt.Printf("Миллисекунд: %.2f
", float64(time.Since(start).Microseconds())/1000)
}

Объяснение: Измерение времени — важнейшая задача. time.Since(start) — самый простой способ. Duration можно преобразовать в наносекунды (.Nanoseconds()), микросекунды (.Microseconds()), миллисекунды (.Milliseconds()) или секунды (.Seconds()). Для точности используйте монотонные часы — Go делает это автоматически.

Сравнение с Python: чем похоже и чем отличается

Если вы уже знакомы с Python, вам будет проще понять Go. Но есть важные отличия:

Характеристика Go (time) Python (datetime)
Основной тип time.Time (всегда дата+время) datetime.date, datetime.time, datetime.datetime
Только дата Нет отдельного типа. Используйте time.Date() с нулевым временем Есть datetime.date
Только время Нет отдельного типа Есть datetime.time
Форматирование Через «магическую дату» 2006-01-02 Через strftime с %Y, %m, %d
Парсинг time.Parse(шаблон, строка) datetime.strptime(строка, шаблон)
Интервалы time.Duration (наносекунды) datetime.timedelta (дни, секунды)
Часовые пояса Встроенная поддержка, база IANA Требуется pytz или zoneinfo (с 3.9)
Монотонные часы Встроены в time.Now() time.monotonic() отдельно
Unix timestamp t.Unix(), t.UnixNano() t.timestamp()
Сравнение Before(), After(), Equal() Операторы <, >, ==

Главные отличия:

  1. Нет отдельной даты: В Go нельзя создать «15 марта» без года. Всегда нужен полный момент времени.
  2. Форматирование: Вместо %Y-%m-%d используется 2006-01-02. Это непривычно, но очень наглядно.
  3. Часовые пояса: В Go они встроены и работают «из коробки». В Python нужно устанавливать pytz.
  4. Монотонные часы: В Go они автоматически используются при измерении интервалов. В Python нужно вызывать time.monotonic() отдельно.
💡 Совет: Если вы переходите с Python на Go, запомните: 2006-01-02 15:04:05 — это ваш новый %Y-%m-%d %H:%M:%S. Напишите эту дату на стикере и приклейте на монитор.

Типичные ошибки и подводные камни

1. Путаница