Содержание урока · 14 разделов
- time в Go: время как значение
- time.Duration: наносекунды под видом удобства
- time.Now, Unix и монотонные часы
- Методы time.Time: Add, Sub, Before, After, Equal
- Таймеры: time.After и time.NewTimer
- Ticker: переносной такт
- Форматирование: раскладка 2006-01-02 15:04:05
- Parse и ParseDuration: из строк во время
- Шпаргалка time
- Правила гигиены времени
- Что дальше: время становится рефлексом
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
time в Go: время как значение#
Пакет time — один из самых обжитых углов стандартной библиотеки: запрос «time go» в поиске почти всегда ведёт к трём сущностям — длительности, моменту и таймеру. Go делает выбор, который поначалу кажется странным, а потом кажется единственно верным: время — это обычные ЗНАЧЕНИЯ, а не глобальное состояние. Момент времени — структура time.Time, которую можно положить в переменную, поле структуры, ключ карты; длительность — отдельный тип time.Duration, который складывается, умножается и печатается; интервалы измеряются не «дата-минус-дата», а вычитанием моментов. Никаких волшебных классов с часовыми поясами внутри каждого вызова — две структуры и набор методов над ними.
У этого выбора есть следствие, которое вы уже видели в прошлом уроке и даже в девятом: &http.Client{Timeout: 5 time.Second} и context.WithTimeout(ctx, 30 time.Second) — это одна и та же валюта. Клиентский таймаут построен на time.Duration, дедлайны контекста построены на time.Duration, и весь остальной код — от ретраев до бэкоффа — тоже. Поэтому урок начинается с Duration, а не с дат: без этой валюты остальное не имеет смысла.
Дорожная карта: сначала time.Duration и его арифметика, затем time.Now, Unix-время и монотонные часы, затем методы time.Time — Add, Sub, Before, After, Equal, затем таймеры time.After и time.NewTimer, метроном time.NewTicker, и в финале — форматирование и time.Parse по раскладке 2006-01-02 15:04:05, на которой спотыкается каждый второй новичок (и каждый опытный — раз в полгода). Мы уже встречали time.After в шестом уроке как таймаут для select; сегодня таймеры распакованы целиком.
Честная рамка урока. Резервный движок песочницы знает время лишь по минимуму: константы time.Second, time.Millisecond, time.Minute, time.Hour (выраженные в МИЛЛИСЕКУНДАХ — 1000, 1, 60000, 3600000), time.Sleep (обрезанный до 50 мс), time.Now и time.Since. Всё остальное недоступно: import "time" с вызовом time.After даёт отказ «строка N: у пакета time нет члена After», а time.Date, time.Parse, time.NewTimer и time.NewTicker отвечают тем же шаблоном «у пакета time нет члена …». Основной движок (yaegi) исполняет пакет time ПОЛНОСТЬЮ и честно: Duration-арифметика, time.Date, Format, Parse, NewTimer, NewTicker, select с time.After — всё работает как в локальном go run, и все нативные примеры урока прогнаны трижды, а таймеры в них идут реальным временем (задержки 10–50 мс, на выходе — не время, а счётчики и факты, поэтому вывод детерминирован). Раннер «1h30m» собран без нативных вызовов и исполняется обоими движками. Таймеры и тикеры идут по настоящим часам браузера — их вывод в этих примерах намеренно построен так, чтобы он не зависел от того, на сколько миллисекунд опаздывает ваш ноутбук.
time.Duration: наносекунды под видом удобства#
time.Duration — это псевдоним простого int64, и единица измерения у него одна: НАНОСЕКУНДА. Никаких полей «часы и минуты» внутри — просто число тиков по миллиардной доле секунды. Отсюда все приятности: Duration ведёт себя как обычное число. Складывайте два интервала — получите суммарный: 2time.Hour + 30time.Minute + 500time.Millisecond. Делите на два — получите половину: pause / 2. Умножайте на целое — pause 2. Сравнивайте < и > — «интервал А короче Б» — это честное числовое сравнение. Единственная валюта снимает целый класс ошибок «секунды против миллисекунд», из-за которых в других языках падали марсианские зонды: сложить time.Second с 500*time.Millisecond можно, а time.Second с 500 — нельзя, типы не совпадут, компилятор остановит вас до продакшена.
Чтобы не считать миллиарды руками, пакет даёт unit-константы: time.Nanosecond, time.Microsecond, time.Millisecond, time.Second, time.Minute, time.Hour. Их значения — те же наносекунды: time.Second — это буквально 1000000000. Большие интервалы собираются умножением: 90 time.Minute, 24 time.Hour — «день», 7 24 time.Hour — «неделя». Константы типа Duration комбинируются с целыми числами в арифметику, которая вычисляется ещё на компиляции, — это обычные константные выражения, а не вызовы функций.
У Duration есть два человеческих интерфейса. Первый — метод String(): fmt.Println(90 * time.Minute) печатает не «5400000000000», а «1h30m0s» — пакет сам подбирает самые крупные единицы и печатает ровно столько знаков, сколько нужно: «300ms», «3.5s», «2h30m0.5s». Второй — разложение по единицам: d.Hours() даёт 1.5 (дробные часы), d.Minutes() — 90, d.Seconds() — 5400, d.Nanoseconds() — сырые 5400000000000. Полезно помнить и потолок типа: int64 наносекунд — это примерно 292 года; ваше время ожидания в него уложится, а вот «вечность» иногда кодируют именно этим максимумом. Обратная дорога — из строки в Duration — это time.ParseDuration("1h30m"), о ней в конце урока; строка «1h30m» в Go — не литерал языка, а контракт функции парсинга, и путать их не стоит.
Что выведет арифметика Duration?
Прочитайте код и выберите, что он выведет:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(90 * time.Minute)
}Длительность из конфига: разбираем 1h30m
Этот раннер исполняют ОБА движка: в коде только fmt, strings и strconv — никаких нативных вызовов time. Это ровно та работа, которую в настоящем коде делает time.ParseDuration: принимает «1h30m» и возвращает Duration. Мы разобрали строку руками, чтобы увидеть, что длительности в конфигах, флагах и JSON — это всегда ТЕКСТ, и на границе программы его кто-то превращает в time.Duration. Эксперимент: поменяйте spec на "2h15m" — разбор подхватит новые числа без изменения кода; а теперь уберите Replace и посмотрите, что Atoi вернёт ноль для "30m" — молчаливый ноль вместо ошибки, классическая ловушка.
Запустите код, чтобы увидеть вывод.
Проверьте себя: что такое Duration
Что хранит time.Duration под капотом и почему это удобно?
time.Now, Unix и монотонные часы#
time.Now() возвращает текущий момент — time.Time, в котором упаковано два этажа информации. Первый этаж — стенное время (wall clock): календарные год, месяц, день, час, минута, секунда и наносекунды в часовом поясе. Второй этаж — добавка от МОНОТОННЫХ часов: та же структура молча носит отсчёт от источника, который не подчиняется ни людям, ни NTP, — он просто идёт вперёд с постоянным темпом. Зачем второй этаж? Стенное время могут переводить: админ передвинул часы на сервере, NTP подрезал дрейф на пару секунд, вступила смена часового пояса — и «сейчас минус двадцать минут назад» может стать отрицательным, нулевым или полуторным. Монотонные часы невосприимчивы к таким трюкам: разность двух time.Now(), взятых в одной программе, честно покажет ПРОТЕКШЕЕ время, потому что t.Sub(t1) видит монотонные отсчёты и предпочитает их. Практическое правило: измеряйте интервалы внутри процесса — time.Since(start) и time.Until(deadline) — и они не соврут от перевода часов; сравнивайте календарные моменты снаружи — и тогда уже нужна раскладка и зоны.
Календарный слой читается полями и методами: t.Year(), t.Month() (тип time.Month, печатающийся словом — «November»), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Weekday() (день недели словом — «Tuesday»). А t.Unix() отдаёт количество СЕКУНД с начала эпохи — полночи 1 января 1970 UTC; это универсальный язык логов, TTL-ключей в кэше и сроках в базах: число, которое не зависит от зоны и легко сравнивается. t.UnixMilli() — те же миллисекунды, как их хранят многие API и JavaScript.
И сразу правило честных примеров: time.Now() недетерминирован — два запуска одной программы дадут разные числа, и ни один вывод в раннерах этого урока не печатает текущий момент. Для обучения, тестов и воспроизводимых демо момент собирают конструктором time.Date(год, месяц, день, час, минута, секунда, наносекунды, зона) — например, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC). Ноль наносекунд и time.UTC — константа зоны;utc-моменты одинаковы везде, в отличие от локальных. Когда увидите в раннере «2009-11-10 23:00:00» — это не прошлоее заклинание, а страховка детерминизма.
Методы time.Time: Add, Sub, Before, After, Equal#
time.Time — структура с методами, и пять из них вы будете открывать постоянно. t.Add(d) сдвигает момент на длительность: t.Add(24 time.Hour) — «завтра в это же время», t.Add(-90 time.Minute) — «полтора часа назад» (отрицательные Duration — легальный путь в прошлое). Важно: метод НЕ меняет получателя — time.Time иммутабелен, Add возвращает НОВЫЙ момент, и писать t = t.Add(d) — норма. t.Sub(u) делает обратное: из двух моментов делает длительность — next.Sub(t) вернёт 24h0m0s, и это снова time.Duration, который можно делить, печатать и складывать с другими интервалами. Пара Add/Sub — это «время плюс-минус время»: момент плюс интервал — момент, момент минус момент — интервал.
Сравнения — три булевых метода: t.Before(u) «раньше», t.After(u) «позже», t.Equal(u) «тот же момент». Почему не операторы? Операторы == и < на структурах в Go не работают, а == на time.Time к тому же сравнивал бы и монотонную добавку — два момента, равные по календарю, могли бы оказаться «не равны» из-за разных отсчётов монотонных часов. Equal сравнивает только календарную суть — именно так и надо спрашивать «это один и тот же момент?». Мелочь, которая экономит баг-репорты: методы Before/After строги, t.Before(t) — всегда false; «не позже» записывается как !t.After(u) или t.Before(u) || t.Equal(u).
Полный набор календарных вопросов: t.Year(), t.Month(), t.Day(), t.Weekday() для даты, t.Hour(), t.Minute(), t.Second() для времени, t.Unix() для эпохи. Из них собираются и «сегодняшняя ночь» (обнулить часы через time.Date по полям t), и «конец месяца», и привычные кэш-ключи «YYYY-MM-DD». Всё это чистые функции над значениями — берите момент, задавайте вопросы, получайте ответы; никакого скрытого состояния, поэтому в тестах такое проверяется элементарно.
Соберите дату и сдвиньте её
Восстановите сборку детерминированного момента и его печать: конструктор time.Date, зона UTC и раскладка для Format.
Таймеры: time.After и time.NewTimer#
Таймер — обещание будильника: через длительность d в канал приедет одно значение time.Time. В шестом уроке вы уже видели каноническую форму case <-time.After(d): внутри select — таймаут на операцию; там же был честный оборот: непроигравший таймер доживает до звонка и мусорится только потом. Таймеры golang на этом не кончаются: time.After годится для одноразовых будильников, но у неё нет кнопок — канал вернётся, а управлять им нельзя. Когда будильник нужен как ОБЪЕКТ — с остановкой и перезапуском, — берите time.NewTimer(d): структура с полем C (канал <-chan time.Time), методом Stop() и методом Reset(d).
Метод Stop() отменяет звонок и возвращает bool: true — успели до звонка, таймер деактивирован; false — поздно, будильник уже прозвенел (или вы его уже вычитали из C). Это не ошибка, а отчёт о гонке: Stop после звонка честно говорит «уже неактуально». Правильный дренаж: если Stop вернул false и вы не читали канал — прочитайте <-timer.C, чтобы гарантированно освободить ресурсы таймера. Reset(d) переводит будильник на новую длительность; канонический паттерн — Stop, дренаж канала, Reset: именно так выглядит «продлить ожидание ещё на d» в очередях и ретраях.
Практический расклад: time.After — внутри select, где таймер одноразовый и живёт ровно один выбор (таймауты на приём, дедлайн на попытку); time.NewTimer — когда таймер переживает одну операцию: отменяемые ожидания, циклы с паузой, которая растёт (экспоненциальный бэкофф), «звонок через d, если ничего не случилось». Есть и time.AfterFunc(d, fn) — таймер, вызывающий функцию вместо канала; упомянем и пойдём мимо: в конкурентном коде канал-будильник управляемее колбэка.
Бэкофф: таймер, который растёт с попыткой
GOpackage main
import (
"fmt"
"time"
)
func main() {
// Пауза перед повтором: растёт вдвое с каждой попыткой.
pause := 10 * time.Millisecond
for attempt := 1; attempt <= 3; attempt++ {
timer := time.NewTimer(pause)
<-timer.C // ждем звонка
timer.Stop() // поздно: вернёт false
fmt.Println("попытка", attempt, "после паузы", pause)
pause *= 2 // 10ms -> 20ms -> 40ms
}
}Арифметика Duration в деле: pause *= 2 удваивает интервал, и печать показывает человеческие «10ms», «20ms», «40ms» — String() подбирает единицы. Каждая попытка получает СВОЙ таймер; когда таймер нужен один на много звонков — Stop, дренаж и Reset, как в тексте урока. В песочнице код не исполняется (таймеры идут реальным временем и резервный движок их не знает), но в настоящем Go он печатает ровно три строки — нативная проба урока это подтвердила трижды.
After в select: гонка со ставкой
Раннер нативный: резервный движок не знает time.After («у пакета time нет члена After»), а основной (yaegi) исполняет select из шестого урока с настоящими таймерами — связка та же, что была в теме select. Два забега: в первом поставщик (20 мс) заведомо быстрее будильника (1 с) — побеждает значение; во втором канал мёртв — побеждает будильник (10 мс). Вывод не зависит от реальных задержек: отрывы в разы, а печатаются факты, а не время. Эксперимент: поставьте After(10 * time.Millisecond) в первом select — гонка станет честной, и запуски начнут чередовать исходы.
Запустите код, чтобы увидеть вывод.
Ticker: переносной такт#
Если Timer — будильник на один звонок, то time.NewTicker(d) — метроном: раз в d в канал C приезжает тик, и так до остановки. Классические потребители: воркер, который «раз в минуту обновляет метрики», «раз в пять секунд шлёт heartbeat», «раз в секунду перерисовывает статус». Внутри Ticker — тот же канал time.Time, но доставляющая горутина пакета повторяет звонки, пока вы не скажете Stop(). И это «пока» — не вежливость: НЕОСТАНОВЛЕННЫЙ тикер держит горутину и канал до конца программы; тикеры, созданные в цикле, — это утечка по одному на итерацию. В отличие от таймера, Stop() у тикера не возвращает ничего — он просто закрывает кран; тикер нельзя ни «перевести», ни «продлить», только остановить и создать новый с другой длительностью.
Второе свойство, о котором стоит знать заранее: тикер НЕ догоняет пропущенные тики. Если читатель занят дольше такта, опоздавшие звонки выбрасываются — в канал встаёт максимум один тик, и ритм идёт «от последней доставки», а не «догоняя счёт». Это правильное поведение для «не чаще, чем раз в d»: нагруженный воркер не получит лавину накопленных тиков, когда освободится. Если нужен строгий счёт «ровно 60 раз», считают приходы, а не пропуски. И ещё: у такта есть якорь — интервал отмеряется от момента создания тикера, поэтому после долгой обработки следующий тик приходит от общего ритма, а не через d после неё; когда нужен именно «пауза ПОСЛЕ работы», берут time.Sleep или таймер в конце итерации.
Семантический выбор «Timer или Ticker» сводится к вопросу «сколько звонков?». Один — таймер или time.After; многие — тикер. Остановка «по событию, а не по счёту» собирается select'ом из шестого урока: ветка тикера и ветка отмены — ctx.Done() из девятого урока; как только контекст истёк, Stop() и выход. Такой воркер — самый частый житель продакшн-кода с пакетом time, и ниже он разобран как код-блок.
Ticker: три тика и остановка
Раннер нативный: резервный движок не знает time.NewTicker («у пакета time нет члена NewTicker»), основной (yaegi) гоняет метроном по настоящим часам. Печать — счётчики, а не время: «тик 1», «тик 2», «тик 3», «остановлен» — вывод одинаковый при любом дрожании планировщика, поэтому пример прогнан трижды. Обратите внимание: тик читается ИЗ канала C — без чтения тикер копит максимум один звонок и стихает. Эксперимент: вынесите Stop в комментарий — программа всё равно завершится (main не ждёт тикер), но горутина тикера останется — в долгой программе это уже утечка.
Запустите код, чтобы увидеть вывод.
Тикер в воркере: такт плюс выход по контексту
GOpackage main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
// Такт раз в секунду: воркер дышит, пока жив контекст.
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
select {
case <-ctx.Done():
fmt.Println("воркер остановлен:", ctx.Err())
return
case <-tk.C:
fmt.Println("такт")
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3500*time.Millisecond)
defer cancel()
go worker(ctx)
time.Sleep(4 * time.Second)
}Собрание урока из трёх тем: тикер (этот урок), select (шестой) и контекст (девятый). Воркер тикает раз в секунду, а по истечении дедлайна контекста уходит по ветке ctx.Done() с ошибкой context deadline exceeded и defer tk.Stop() на выходе — канал закрыт, горутина завершилась, утечки нет. В песочнице код не исполняется: резервный движок не знает ни time.NewTicker, ни context, а таймеры идут реальным временем; в настоящем Go он печатает три «такт» и финальную строку.
Форматирование: раскладка 2006-01-02 15:04:05#
Форматирование времени в Go — самая знаменитая «мистика» языка: вместо символов-подстановок вроде %Y-%m-%d функция t.Format(...) принимает ОБРАЗЕЦ — референсное время, записанное в нужном вам виде. Раскладка 2006-01-02 15:04:05 печатает момент как «2009-11-10 23:00:00», и выглядит это как обычный шаблон даты. Но за образцом стоит точная логика: эталоном выбрано понедельник 2 января 2006 года, 15:04:05 в зоне MST — момент, элементы которого идут по порядку АНГЛИЙСКОЙ мнемоники: 1 — месяц, 2 — день, 3 — час (15 часов это 3 PM), 4 — минута, 5 — секунда, 6 — год (’06), 7 — зона (-0700). Канонический супер-шаблон «01/02 03:04:05PM '06 -0700» читается как счёт от одного до семи; от него и отталкиваются: хотите месяц-день-год — берите элементы «01 02 06», хотите только время — «15:04:05». Референсное время — не дата выпуска Go и не шутка ради шутки: это самодокументирующийся формат, где образец одновременно является описанием.
Правила элементарны, а ошибки классичны. Первая: элементы раскладки — КОНСТАНТЫ, их нельзя «придумывать»: «2006-01-02» работает, «2006-1-2» и «YYYY-MM-DD» — нет. Вторая: порядок имеет значение — Format("2006-02-01") на моменте 2009-11-10 честно напечатает «2009-10-11»: месяц и день поменялись местами, потому что вы поменяли их в образце; компилятор вас не остановит — раскладка это данные, а не синтаксис. Третья: 15 — это 24-часовой час, 03 — 12-часовой с ведущим нулём (и нужен PM-маркер), 04 — минуты (не месяц!), 05 — секунды. Набор готовых констант закрывает большую часть жизни: time.RFC3339 («2006-01-02T15:04:05Z07:00») — стандарт API и логов, time.Kitchen («3:04PM») — для веселья в примерах документации.
Печать — t.Format(раскладка) — и чтение — time.Parse(раскладка, строка) — симметричны: одна и та же раскладка описывает обе стороны круговорота. Parse возвращает пару (time.Time, error), и ошибка там — норма жанра: строка «01.03.2024» против раскладки «2006-01-02» даёт ошибку parsing time "01.03.2024" as "2006-01-02": cannot parse "01.03.2024" as "2006" — движок честно показывает, где образец перестал совпадать. Дата-время без зоны парсится в UTC; строка со смещением («2024-03-01T09:30:00+03:00») — в ту зону, что указана. Правило гигиены: одна раскладка на один канал данных — та, что договорена с API, конфигом или логом; между системами — RFC3339, а локальные вкусности вроде «02.01.2006» — только для глаз человека.
Проверьте себя: почему именно 2006-01-02
Почему раскладка форматирования в Go выглядит как 2006-01-02 15:04:05?
Format и Parse: туда и обратно
Раннер нативный: резервный движок не знает ни time.Date, ни time.Parse («у пакета time нет члена …»), основной (yaegi) исполняет круговорот честно. Четыре строки: момент по раскладке, Unix-секунды и день недели, обратный Parse с пятницей, и ошибка разбора строки, написанной не по образцу. Вывод полностью детерминирован — ни одного вызова time.Now(). Эксперимент: напечатайте t.Format("2006-02-01") и объясните себе «2009-10-11» — месяц и день поменялись местами вместе с элементами раскладки.
Запустите код, чтобы увидеть вывод.
Parse и ParseDuration: из строк во время#
Граница программы — всегда строки: конфиг, флаг, JSON, поле формы. Пакет time даёт два конвертера, и важно их не путать. time.ParseDuration(s) превращает строку в ДЛИТЕЛЬНОСТЬ: "1h30m" — полтора часа, "90s" — тот же интервал, печатающийся «1m30s», "300ms" — треть секунды, "-2m" — назад во времени. Единицы — ns, us, ms, s, m, h; дроби легальны («1.5h»), компоненты складываются («1h30m» это 1 час плюс 30 минут). Дня нет: "1d" даёт ошибку time: unknown unit "d" in duration "1d" — «день» намеренно отсутствует, потому что день бывает 23, 24 и 25 часов из-за перевода часов; если нужен календарный день — это уже не Duration, а арифметика дат: t.AddDate(0, 0, 1). Ошибка ParseDuration — нормальный возврат второго значения, и её надо проверять так же, как err из Atoi.
time.Parse(layout, value) превращает строку в МОМЕНТ и работает строго по раскладке из предыдущего блока: та же грамматика элементов, тот же референс. Практические пары: раскладка «2006-01-02» для дат без времени, «2006-01-02 15:04:05» для серверных логов, time.RFC3339 для всего, что ходит по сети. Моменты со смещением зоны парсятся в эту зону и сравниваются через Equal корректно независимо от представления — «15:00+03:00» и «12:00Z» это один момент. Не путайте с time.ParseInLocation(layout, value, loc) — она нужна, когда строка без зоны, но вам известно, в какой зоне она написана (например, локальные логи сервера): без неё «2024-03-01 09:30:00» станет UTC-полуночным сдвигом на ваш офсет.
Оба конвертера живут на границах, и на границах же они ошибаются. Правила безопасности: проверяйте err первым делом; не парсьте одно и то же поле двумя раскладками («сначала попробую так, потом эдак» — путь к данным, которые месяц лежали правильной строкой, а потом кто-то поменял порядок); длительности в JSON храните строкой с единицами («"timeout": "30s"») или числом наносекунд, но не «секундами, которые на самом деле миллисекунды» — это та самая ловушка единственной валюты, от которой Duration вас защищает внутри языка.
Соберите напоминание из строк
Соберите программу: строка «50ms» превращается в Duration, таймер звонит, мы его останавливаем (поздновато) и печатаем итог. Нативный вывод: «напоминание: 50ms — время вышло».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Шпаргалка time#
| Инструмент | Что делает | Пример | Нюанс |
|---|---|---|---|
| time.Duration | int64-наносекунды с методами | 90 * time.Minute | печатается «1h30m0s» |
| time.Second / Minute / Hour | unit-константы Duration | 2time.Hour + 30time.Minute | арифметика как у чисел |
| d.Hours() / Seconds() | разложение по единицам | d.Hours() → 1.5 | дробные значения |
| time.ParseDuration | строка → Duration | ParseDuration("90s") | «1d» — ошибка: дня нет |
| time.Now() | текущий момент (стена+монотонная) | start := time.Now() | недетерминирован в выводе |
| t.Unix() | секунды с 1970 UTC | t.Unix() | язык логов и TTL |
| time.Date(...) | собрать момент из чисел | Date(2009, time.November, 10, ...) | детерминизм в примерах |
| t.Add(d) / t.Sub(u) | сдвиг момента / разность | t.Add(24 * time.Hour) | Time иммутабелен |
| t.Before / After / Equal | сравнение моментов | t.Equal(u) | Equal вместо == |
| time.After(d) | одноразовый будильник-канал | case <-time.After(d): | select из шестого урока |
| time.NewTimer | таймер с кнопками | timer.Stop() / timer.Reset(d) | Stop после звонка = false |
| time.NewTicker | метроном | tk := time.NewTicker(d) | Stop обязателен; тики не копятся |
| t.Format / time.Parse | строка ↔ время по раскладке | Format("2006-01-02") | референсное время; RFC3339 |
Правила гигиены времени#
- Одна валюта — наносекунды: считайте интервалы в time.Duration и unit-константах, не в float-секундах и не в «числах, которые на самом деле миллисекунды».
- Строки длительностей превращайте в Duration на границе — time.ParseDuration и проверка err; руками не собирайте, если есть стандартный парсер.
- time.Now() недетерминирован: в демо, тестах и примерах собирают моменты через time.Date — вывод не зависит от календаря запуска.
- Интервалы внутри процесса меряйте time.Since(start) и time.Until(deadline): монотонная добавка честна против перевода часов и NTP.
- Сравнение моментов — Before/After/Equal, не ==: Equal игнорирует монотонную добавку и сравнивает календарную суть.
- time.Time иммутабелен: Add возвращает новый момент — присваивайте t = t.Add(d), иначе сдвиг потеряется.
- time.After — для одноразовых будильников в select; таймер, который переживает операцию, — time.NewTimer с Stop и Reset.
- Stop() у таймера возвращает false, если звонок уже случился, — это не ошибка, а отчёт о гонке; при необходимости дочитайте канал.
- Тикер без Stop() — утечка горутины: останавливайте метроном явно, особенно в циклах и воркерах.
- Тикер не догоняет пропущенные тики: это «не чаще, чем раз в d»; для «пауза после работы» — таймер или Sleep в конце итерации.
- Раскладка Format/Parse — референсное время 2006-01-02 15:04:05: элементы-константы, порядок имеет значение, «2006-02-01» честно поменяет месяц и день.
- Между системами — time.RFC3339; локальные раскладки — только для глаз человека; одна раскладка на один канал данных.
- «1d» в ParseDuration не существует: день бывает разной длины — календарные дни считаются AddDate, а не Duration.
- Песочница исполняет time честно только основным движком (yaegi): таймеры и тикеры идут реальным временем, а примеры прогнаны трижды.
Что дальше: время становится рефлексом#
Соберём урок в одну карту. Валюта: time.Duration — int64-наносекунды, unit-константы от Nanosecond до Hour, арифметика как у чисел, печать через String() («1h30m0s»), разложение Hours()/Minutes()/Seconds(). Момент: time.Now() с двумя этажами — стенным и монотонным, Unix() для эпохи, time.Date для детерминированных примеров. Методы: Add и Sub (момент плюс интервал, момент минус момент), Before/After/Equal для сравнений. Таймеры: time.After — одноразовый будильник для select из шестого урока, time.NewTimer — объект с C, Stop() и Reset(); бэкофф собирается удвоением Duration. Метроном: time.NewTicker с обязательным Stop() и честной природой «не чаще, чем раз в такт». Круговорот строк: Format и time.Parse по раскладке 2006-01-02 15:04:05 — референсное время, элементы которого сами подсказывают порядок; ParseDuration для конфигов, RFC3339 для сети.
Двадцать четвертый урок — идиомы Go: приёмы, которые отличают код «на языке» от кода «по учебнику», — соберём в одну страницу привычки всех предыдущих тем, от defer до контекста. Таймеры в этой копилке уже лежат: «таймаут на операцию — select с time.After», «такт воркера — тикер со Stop», «дедлайн — Duration, а не магическое число». А если захочется увидеть, где время живёт в сети, вернитесь к уроку про HTTP-клиент: Client.Timeout — это Duration в роли бюджета. Про отмену по дедлайну — урок про контекст: WithTimeout тоже питается временем из этого пакета. Про select и будильник в одной конструкции — урок про select: таймаут на операцию был первым таймером курса.
До встречи на странице курса. Домашняя работа урока — маленькая библиотека времени: соберите функции с Duration, таймерами и раскладками, прогоните их в настоящем Go и объясните каждую строку с «2006» в ней.
Карточки урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- time.Duration — int64-наносекунды: арифметика и сравнения как у чисел, unit-константы от Nanosecond до Hour, печать через String() («1h30m0s»), разложение Hours()/Minutes()/Seconds().
- Одна валюта времени защищает от ошибок «секунды против миллисекунды»: сложить Duration с голым числом нельзя — компилятор остановит.
- time.Now() несёт стенное время и монотонную добавку: time.Since и time.Until меряют интервалы честно даже при переводе часов и NTP.
- t.Unix() — секунды с 1970 UTC для логов и TTL; time.Date(..., time.UTC) — детерминированный конструктор моментов для примеров и тестов.
- Методы time.Time: Add(d) возвращает новый момент (Time иммутабелен), Sub(u) даёт Duration, Before/After/Equal сравнивают корректно (Equal вместо ==).
- time.After(d) — одноразовый будильник-канал для select (таймаут из шестого урока); time.NewTimer — объект с C, Stop() и Reset(d).
- Stop() таймера возвращает false, если звонок уже случился, — это отчёт о гонке, а не ошибка; Reset по канону — Stop, дренаж, Reset.
- time.NewTicker — метроном: тик в канал C раз в d до Stop(); не остановленный тикер — утечка горутины, Stop в циклах обязателен.
- Тикер не догоняет пропущенные тики — семантика «не чаще, чем раз в d»; для паузы «после работы» берут Sleep или таймер в конце итерации.
- Форматирование — по референсному времени: 2006-01-02 15:04:05 — это 1 месяц, 2 день, 3 час (15:04:05), 6 год, 7 зона; элементы-константы, порядок значим.
- Format и time.Parse симметричны (одна раскладка на обе стороны), time.RFC3339 — стандарт для API; time.ParseDuration парсит «1h30m» и «90s», но «1d» — ошибка: дней в Duration нет.
- Песочница: резервный движок знает только константы (в миллисекундах), Sleep, Now и Since; основной (yaegi) исполняет time полностью — таймеры и тикеры идут реальным временем, примеры прогнаны трижды.
Домашнее задание
Часть первая — валюта: вычислите «1 день и 3 часа» из unit-констант, напечатайте через String(), Hours() и Seconds() и объясните в комментарии, почему деление d/2 легально, а d/2 + 5 — нет. Часть вторая — детерминизм: соберите момент своего рождения через time.Date в time.UTC, напечатайте Weekday и раскладкой «2006-01-02»; добавьте 10000 часов через Add и объясните, почему присваивание t = t.Add(...) обязательно. Часть третья — мистика: напечатайте один и тот же момент раскладками «2006-01-02 15:04:05», «02.01.2006 15:04», «15:04:05», time.RFC3339 и «2006-02-01»; к последней напишите комментарий, почему вышло «поменялись месяц и день». Часть четвёртая — гонка: соберите select из value-канала и time.After(50 * time.Millisecond), где поставщик спит 10 миллисекунд; объясните, почему исход стабилен, и что изменит After(5 * time.Millisecond). Часть пятая — метроном: тикер на 20 миллисекунд с пятью тиками, явный Stop() и объяснение, что произойдёт без него в долгой программе; затем соберите воркер с select по тикеру и ctx.Done() из девятого урока. Часть шестая — строки: time.ParseDuration для «90s», «1h30m» и «1d» (ошибку — проверить и объяснить), time.Parse по раскладке урока и разбор ошибки для «01.03.2024». Прогоните всё в настоящем Go; комментарии в коде — по-русски.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему раскладка форматирования — именно 2006-01-02 15:04:05, а не %Y-%m-%d?
Потому что в Go формат — это не набор директив, а образец: референсное время «понедельник 2 января 2006, 15:04:05 MST». Его элементы идут по возрастанию английской мнемоники: 1 — месяц, 2 — день, 3 — час (15:00 = 3 PM), 4 — минута, 5 — секунда, 6 — год (’06), 7 — зона (-0700); канонический супер-шаблон «01/02 03:04:05PM '06 -0700». Так образец сам себя документирует: вы пишете дату в том виде, в котором хотите её увидеть. Это не дата выпуска Go (она — 10 ноября 2009, любимая дата примеров) и не требование RFC 3339: time.RFC3339 — просто одна из готовых констант раскладки.
Чем time.NewTimer отличается от time.NewTicker и когда что выбирать?
Вопрос «сколько звонков?». Timer — будильник на один звонок: через d в канал C приедет одно значение; им управляют Stop() (отменить, вернёт bool — успели ли) и Reset(d) (перевести). Ticker — метроном: тик раз в d, пока не вызван Stop(); его нельзя «продлить» или «перевести», только остановить и создать новый. time.After(d) — упрощённый одноразовый таймер для select без кнопок. Практика: таймаут на одну операцию — time.After; отменяемое ожидание и бэкофф — NewTimer; периодический воркер (heartbeat, метрики) — NewTicker с обязательным Stop(), иначе горутина тикера утечёт.
Что такое монотонные часы и где они реально важны?
time.Now() кроме календарного (стенного) времени несёт отсчёт монотонных часов — источника, который только идёт вперёд и не подчиняется ни переводу часов, ни NTP. Разности moments — t.Sub(u), time.Since(start), time.Until(deadline) — видят монотонные отсчёты и потому честно показывают протекшее время, даже если стенные часы за это время передвинули. Это критично для таймаутов, профилирования и любых SLA-логик: дедлайн «плюс 30 секунд» не превращается в «плюс 2 часа» после прыжка часов. Календарные вопросы («какая сегодня дата») всегда отвечаются стенным временем и его зонами.
Работает ли пакет time в песочнице самоучителя?
По-разному в двух движках, и урок говорит об этом честно. Резервный движок знает минимум: константы time.Second/Minute/Hour/Millisecond (выражены в миллисекундах — 1000, 60000, 3600000, 1), time.Sleep (обрезан до 50 мс), time.Now и time.Since; вызовы time.After, time.Date, time.Parse, time.NewTimer, time.NewTicker дают отказ «у пакета time нет члена …». Основной движок (yaegi) исполняет time полностью и честно: Duration-арифметика, time.Date, Format, Parse, таймеры и тикеры по реальным часам; все нативные примеры урока прогнаны трижды, а их вывод построен от счётчиков и фактов, а не от текущего момента, поэтому детерминирован. Таймеры на секунды и минуты проверяйте в настоящем Go.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Fiber: экспресс-подобный веб-фреймворк на Fasthttp
Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.
- Поднимать HTTP-сервер на Fiber за минуты
- Проектировать маршруты, группы и версии API
- Писать middleware: логи, CORS, лимиты, recovery
- Принимать и отдавать JSON с валидацией