Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~20 мин +50 XP

time и таймеры

Запрос «time go» в стандартной библиотеке занимает то же место, что и таймеры golang в любом веб-фреймворке: без времени не работают ни таймауты, ни кэши, ни метрики, ни дедлайны. Двадцать третий урок разбирает пакет time целиком: time.Duration как int64-наносекунды с человеческим лицом, time.Now и Unix-время, методы time.Time — Add, Sub, Before, After, Equal, — таймеры time.After и time.NewTimer, метроном time.NewTicker со своевременным Stop и знаменитое форматирование по раскладке 2006-01-02 15:04:05, где числа сами рассказывают, что подставлять. Связка с прошлым уроком прямая: клиентский таймаут из двадцать второго урока был 5 * time.Second — теперь вы узнаете, что это за конструкция на самом деле и что с ней можно делать ещё.

Содержание урока · 14 разделов

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.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.

package main import ( "fmt" "time" ) func main() { // Собираем момент из чисел в зоне UTC. t := (2009, time.November, 10, 23, 0, 0, 0, ) fmt.Println(t.Format("2006-01-02 15:04:05")) // Два часа вперёд — и печатаем той же раскладкой. fmt.Println(t.Add(2 * time.Hour).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) — таймер, вызывающий функцию вместо канала; упомянем и пойдём мимо: в конкурентном коде канал-будильник управляемее колбэка.

Бэкофф: таймер, который растёт с попыткой

GO
package 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 не ждёт тикер), но горутина тикера останется — в долгой программе это уже утечка.

Вывод

Запустите код, чтобы увидеть вывод.

Тикер в воркере: такт плюс выход по контексту

GO
package 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.Durationint64-наносекунды с методами90 * time.Minuteпечатается «1h30m0s»
time.Second / Minute / Hourunit-константы Duration2time.Hour + 30time.Minuteарифметика как у чисел
d.Hours() / Seconds()разложение по единицамd.Hours() → 1.5дробные значения
time.ParseDurationстрока → DurationParseDuration("90s")«1d» — ошибка: дня нет
time.Now()текущий момент (стена+монотонная)start := time.Now()недетерминирован в выводе
t.Unix()секунды с 1970 UTCt.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» в ней.

Карточки урока

Термин

time.Duration: что под капотом

нажмите, чтобы перевернуть
Определение

Псевдоним int64, единица — наносекунда. Арифметика как у чисел: сумма 2*time.Hour + 30*time.Minute, деление pause/2, умножение pause*2, сравнения < >. Unit-константы: time.Nanosecond … time.Hour (time.Second = 1000000000 нс). Печать — метод String(): «1h30m0s», «300ms», «3.5s»; разложение — Hours()/Minutes()/Seconds()/Nanoseconds(). Потолок ~292 года. Обратный путь — time.ParseDuration("1h30m").

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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; комментарии в коде — по-русски.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Pro · time и таймеры)

Вопросы и ответы по уроку

Почему раскладка форматирования — именно 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.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией