---
title: "time и таймеры"
description: "time в Go: Duration и арифметика, time.Now и Unix, методы time.Time, таймеры After и NewTimer, такт Ticker со Stop, Format и Parse по раскладке 2006-01-02."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/23-time/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# time и таймеры

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 23 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/23-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 — теперь вы узнаете, что это за конструкция на самом деле и что с ней можно делать ещё.

### 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 ведёт себя как обычное число. Складывайте два интервала — получите суммарный: 2*time.Hour + 30*time.Minute + 500*time.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?

```go
package main

import (
        "fmt"
        "time"
)

func main() {
        fmt.Println(90 * time.Minute)
}
```

**Что выведет программа?**

- ✅ 1h30m0s — Duration печатает себя по-человечески через String()
- 5400000000000 — сырые наносекунды
- 90m — сокращённая запись без секунд
- 1.5 — значение в часах

90 * time.Minute — это значение типа time.Duration, а у него есть метод String(): fmt.Println печатает «1h30m0s» — крупные единицы первыми, дробь только там, где она есть. Вариант с наносекундами был бы верен, если бы Duration печатался как число, — но у типа есть String(). Честная сноска: в резервном движке песочницы Duration — это число МИЛЛИСЕКУНД без метода String(), и тот же код напечатает 5400000 — ещё одно доказательство, что резерв упрощён. Проверяйте predict в основном движке (yaegi) — там вывод канонический и прогнан трижды.

### Длительность из конфига: разбираем 1h30m

Этот раннер исполняют ОБА движка: в коде только fmt, strings и strconv — никаких нативных вызовов time. Это ровно та работа, которую в настоящем коде делает time.ParseDuration: принимает «1h30m» и возвращает Duration. Мы разобрали строку руками, чтобы увидеть, что длительности в конфигах, флагах и JSON — это всегда ТЕКСТ, и на границе программы его кто-то превращает в time.Duration. Эксперимент: поменяйте spec на "2h15m" — разбор подхватит новые числа без изменения кода; а теперь уберите Replace и посмотрите, что Atoi вернёт ноль для "30m" — молчаливый ноль вместо ошибки, классическая ловушка.

```go
package main

import (
        "fmt"
        "strconv"
        "strings"
)

func main() {
        // Длительность из конфига приходит строкой: "1h30m".
        spec := "1h30m"
        parts := strings.Split(spec, "h")
        hours, _ := strconv.Atoi(parts[0])
        // "30m": Atoi споткнётся о букву — срезаем суффикс сами.
        minutes, _ := strconv.Atoi(strings.Replace(parts[1], "m", "", 1))
        total := hours*60 + minutes
        fmt.Println(spec, "->", total, "минут")
        fmt.Println(total/60, "ч", total%60, "мин")
}
```

### Проверьте себя: что такое Duration

**Вопрос:** Что хранит time.Duration под капотом и почему это удобно?

- ✅ int64-наносекунды: Duration ведёт себя как число — складывается, делится, сравнивается, а единицы дают константы пакета
- Структуру с полями Hours, Minutes, Seconds — потому и печатается красиво
- float64 в секундах — поэтому арифметика неточная
- Строку вида "1h30m0s" — String() просто её возвращает

Duration — псевдоним int64 с единицей «наносекунда». Красивая печать — это метод String(), который подбирает единицы при выводе, а не устройство хранения. Именно числовая природа даёт главную выгоду: арифметика и сравнения работают без конвертаций, а попытка сложить 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.

```go
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.Date, зона — time.UTC, а раскладка Format — это референсное время урока.

Правильные ответы в пропусках: `time.Date`, `time.UTC`, `2006-01-02 15:04:05`

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

```go
package main

import (
        "fmt"
        "time"
)

func main() {
        ch := make(chan int)
        // Медленный поставщик: 20 мс на раздумья.
        go func() {
                time.Sleep(20 * time.Millisecond)
                ch <- 7
        }()
        // Кто первый готов — тот и победил: значение или будильник.
        select {
        case v := <-ch:
                fmt.Println("значение", v)
        case <-time.After(1 * time.Second):
                fmt.Println("таймаут")
        }

        ch2 := make(chan int)
        select {
        case <-ch2:
                fmt.Println("не придёт")
        case <-time.After(10 * time.Millisecond):
                fmt.Println("таймаут")
        }
}
```

### 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 (
        "fmt"
        "time"
)

func main() {
        tk := time.NewTicker(10 * time.Millisecond)
        count := 0
        for count < 3 {
                // Каждый приём из C — один такт метронома.
                <-tk.C
                count++
                fmt.Println("тик", count)
        }
        // Стоп обязателен: иначе горутина тикера живёт вечно.
        tk.Stop()
        fmt.Println("остановлен")
}
```

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

```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?

- ✅ Это референсное время: 01 — месяц, 02 — день, 15:04:05 — 3-й час 4-я минута 5-я секунда, 06 — год; элементы сами подсказывают порядок
- Дата выпуска Go в открытый доступ — формат выбрали в честь события
- Так требует RFC 3339, а Go просто следует стандарту
- Произвольный шаблон, эквивалентный %Y-%m-%d в стиле C

Эталон — понедельник 2 января 2006, 15:04:05 MST: месяц=01, день=02, час=15 (3 PM), минута=04, секунда=05, год=’06, зона=-0700 — семь элементов по возрастанию. Формат самодокументируется: образец и есть описание. Он не дата выпуска (Go открылся 10 ноября 2009 — это другая любимая дата примеров), не требование RFC 3339 (это лишь одна из готовых констант time.RFC3339) и не шаблон в стиле printf: элементы — фиксированные константы раскладки.

### Format и Parse: туда и обратно

Раннер нативный: резервный движок не знает ни time.Date, ни time.Parse («у пакета time нет члена …»), основной (yaegi) исполняет круговорот честно. Четыре строки: момент по раскладке, Unix-секунды и день недели, обратный Parse с пятницей, и ошибка разбора строки, написанной не по образцу. Вывод полностью детерминирован — ни одного вызова time.Now(). Эксперимент: напечатайте t.Format("2006-02-01") и объясните себе «2009-10-11» — месяц и день поменялись местами вместе с элементами раскладки.

```go
package main

import (
        "fmt"
        "time"
)

func main() {
        // Дата собирается из чисел: год, месяц (time.November!), день...
        t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
        fmt.Println(t.Format("2006-01-02 15:04:05"))
        fmt.Println(t.Unix(), t.Weekday())
        // Обратная дорога: строка по той же раскладке.
        p, err := time.Parse("2006-01-02 15:04:05", "2024-03-01 09:30:00")
        fmt.Println(err, p.Weekday())
        // Строка не по раскладке — ошибка ещё на Parse.
        _, err2 := time.Parse("2006-01-02", "01.03.2024")
        fmt.Println(err2 != nil)
}
```

### 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 — время вышло».

Правильный порядок строк:

1. `package main`
2. `import (`
3. `        "fmt"`
4. `        "time"`
5. `)`
6. ``
7. `func main() {`
8. `        // Строка из конфига -> Duration.`
9. `        d, _ := time.ParseDuration("50ms")`
10. `        // Будильник на эту длительность.`
11. `        timer := time.NewTimer(d)`
12. `        <-timer.C`
13. `        // Поздно: звонок уже случился, Stop вернёт false.`
14. `        timer.Stop()`
15. `        fmt.Println("напоминание:", d, "— время вышло")`
16. `}`

### Шпаргалка time

| Инструмент | Что делает | Пример | Нюанс |
| --- | --- | --- | --- |
| time.Duration | int64-наносекунды с методами | 90 * time.Minute | печатается «1h30m0s» |
| time.Second / Minute / Hour | unit-константы Duration | 2*time.Hour + 30*time.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-клиент](/courses/go-advanced/lessons/22-http-klient/): Client.Timeout — это Duration в роли бюджета. Про отмену по дедлайну — [урок про контекст](/courses/go-advanced/lessons/09-context/): WithTimeout тоже питается временем из этого пакета. Про select и будильник в одной конструкции — [урок про select](/courses/go-advanced/lessons/06-select/): таймаут на операцию был первым таймером курса.

До встречи на [странице курса](/courses/go-advanced/). Домашняя работа урока — маленькая библиотека времени: соберите функции с 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").
- **time.Now, Unix и монотонные часы** — time.Now() — стенное время + тихая добавка от монотонных часов. Монотонные не подчиняются переводу часов и NTP: time.Since(start) и time.Until(deadline) меряют честно протекшее время. t.Unix() — секунды с 1970 UTC (для логов и TTL), t.UnixMilli() — миллисекунды. Календарь: t.Year()/Month()/Day()/Weekday()/Hour()/Minute(). time.Now() недетерминирован — в примерах и тестах собирают момент через time.Date(..., time.UTC).
- **Методы time.Time: Add, Sub, Before, After, Equal** — t.Add(d) — новый момент (Time иммутабелен: t = t.Add(d)); отрицательный d — в прошлое. t.Sub(u) — Duration из двух моментов («24h0m0s»). Сравнения — строгие методы t.Before(u), t.After(u), t.Equal(u); Equal вместо ==, потому что == зацепил бы монотонную добавку. «Не позже» — !t.After(u).
- **Timer: time.After vs time.NewTimer** — time.After(d) — одноразовый будильник-канал для select (case <-time.After(d): — таймаут из урока 06); кнопок нет. time.NewTimer(d) — объект с каналом C, Stop() и Reset(d). Stop() возвращает bool: true — успели до звонка, false — уже прозвенел (не ошибка, а отчёт о гонке; при необходимости дочитайте <-timer.C). Канонический Reset — Stop, дренаж, Reset. Бэкофф — удвоение Duration между попытками.
- **Ticker: метроном и Stop** — time.NewTicker(d) — канал C, в который раз в d приезжает тик, пока не вызван Stop(). Не остановленный тикер держит горутины — утечка (Stop в циклах обязателен). Тикер НЕ догоняет пропущенные тики (максимум один ждёт в канале) — семантика «не чаще, чем раз в d»; для «пауза после работы» — Sleep/таймер в конце итерации. Остановка воркера — select: ветка tk.C и ветка ctx.Done() из урока 09.
- **Референсное время: Format, Parse, ParseDuration** — t.Format(раскладка) печатает по ОБРАЗЦУ: 2006-01-02 15:04:05 — это эталон «понедельник 2 января 2006, 15:04:05 MST»: 1=месяц, 2=день, 3=час(15), 4=минута, 5=секунда, 6=год, 7=зона. Элементы — константы, порядок значим: Format("2006-02-01") даст «2009-10-11» на 2009-11-10. time.Parse(layout, s) — обратный путь, ошибка вида cannot parse ... as "2006" — норма. time.RFC3339 — константа для API. time.ParseDuration("90s") → «1m30s»; «1d» — ошибка: дня нет (дни — AddDate).

## Коротко: что запомнить

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