1. Зачем нужны таймеры и тикеры, если есть Sleep
Когда вы впервые видите “ожидание по времени” в Go, кажется, что всё решает один инструмент: time.Sleep(d). Он правда полезен, но это скорее “поставить программу на паузу”. А в реальных задачах мы часто хотим не просто паузу, а событие: «через 2 секунды сделай действие» или «каждые 5 секунд делай проверку».
Вот тут и появляются таймеры и тикеры: они дают нам аккуратный механизм ожидания, который легко встроить в логику цикла и который читается как “ждём сигнал времени”.
Представьте разницу на бытовом уровне. Sleep — это как “закрыть глаза и считать до десяти”. After — это как “поставить будильник и дождаться звонка”. Ticker — как “метроном”: тик-тик-тик, пока не остановите.
Канал‑сигнал: что возвращают time.After и ticker.C
Сейчас будет одно слово, которое вы ещё не разбирали системно: канал. Полноценные каналы будут позже, но сегодня нам нужен “локальный минимум”, чтобы не превращать лекцию в сериал на 12 сезонов.
time.After(d) возвращает значение типа “канал времени” (на практике это <-chan time.Time). У time.NewTicker(d) есть поле C, которое тоже является таким каналом (тоже <-chan time.Time). Упрощаем: это штука, из которой можно один раз (или много раз) получить сигнал “время наступило”.
Главное правило сегодняшней лекции: мы не создаём каналы сами, не отправляем в них значения и не закрываем их. Мы делаем только одно действие: читаем событие оператором <-.
Пример чтения выглядит так:
<-time.After(200 * time.Millisecond)
Это читается почти как человеческий язык: “подожди, пока не пройдёт 200ms”.
Для визуального представления пусть будет маленькая схема:
flowchart LR
A[код дошёл до ожидания] --> B["<-time.After(d)"]
B -->|блокируется| C[проходит d]
C -->|сигнал в канале| D[код продолжает выполнение]
2. time.After: одноразовый “звонок будильника”
time.After(d) — это самый простой “таймер”: он говорит “когда пройдёт d, в канал придёт одно событие”. Обычно мы даже не сохраняем канал в переменную: мы просто читаем из него и идём дальше.
Важно уловить интонацию. time.Sleep(d) — это “спать d”. time.After(d) — это “получить событие после d”. На уровне вашего кода результат часто один (вы ждёте), но “событийная” модель лучше расширяется: позже, когда появится select, можно будет “ждать либо таймер, либо что-то ещё”. Сегодня select мы не трогаем, но стиль уже закладываем.
Самый честный пример: подождать и продолжить
Начнём с максимально простого кода: дождались события и продолжили.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("start") // start
<-time.After(200 * time.Millisecond) // ждём событие времени
fmt.Println("done") // done
}
Здесь важно, что <-time.After(...) блокирует выполнение так же, как Sleep, но психологически вы привыкаете к паттерну “я жду сигнал”.
Таймер возвращает time.Time: можно увидеть, когда сработало
Событие времени — это не просто “пинг”. В канал приходит момент времени, когда таймер сработал (тип time.Time). Это иногда удобно для логов и диагностики.
package main
import (
"fmt"
"time"
)
func main() {
firedAt := <-time.After(100 * time.Millisecond)
fmt.Println("timer fired at:", firedAt.Format(time.RFC3339Nano))
}
Мы не углубляемся в форматирование (это было отдельной темой), но приятно понимать: таймер — не “магия”, он вполне конкретен.
Дождаться дедлайна: time.Until + time.After
Частая задача: у нас есть конкретный момент deadline, и мы хотим дождаться его наступления. Для этого удобно вычислить длительность “сколько осталось” и подождать её.
package main
import (
"fmt"
"time"
)
func main() {
deadline := time.Now().Add(150 * time.Millisecond)
<-time.After(time.Until(deadline))
fmt.Println("deadline reached") // deadline reached
}
Если дедлайн уже прошёл, time.Until(deadline) будет отрицательной. Это уже тонкость: в реальном коде обычно проверяют знак и решают, есть ли смысл ждать вообще.
3. time.NewTicker: регулярные “тики” по расписанию
Если time.After — это “сработай один раз”, то time.NewTicker(d) — это “срабатывай каждые d”. Тикер полезен, когда вам нужно что-то делать регулярно: печатать прогресс, обновлять кэш, делать периодическую проверку состояния, отправлять отчёт и так далее.
В реальных системах тикеры встречаются очень часто: например, периодические фоновые операции могут быть завязаны именно на тикер.
Технически time.NewTicker(d) возвращает *time.Ticker, у которого есть поле C — канал событий. Мы читаем из ticker.C так же, как читали из time.After.
И сразу важное правило гигиены: тикер нужно останавливать. Для этого есть ticker.Stop(). Обычно это делается через defer, чтобы “точно остановили”, даже если выйдем из функции раньше.
Три тика — и хватит
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for i := 1; i <= 3; i++ {
<-ticker.C
fmt.Println("tick", i) // tick 1, tick 2, tick 3
}
}
Здесь вы видите канонический паттерн: создали тикер, defer Stop(), в цикле ждём <-ticker.C.
Мини‑приложение: прогресс‑бар без прогресс‑бара
Давайте начнём связывать примеры в “одно приложение”, которое развивается. Пусть у нас будет маленькая CLI‑программа, которая имитирует “долгую работу” (например, обработку данных) и печатает прогресс раз в фиксированный интервал.
Да, это пока игрушка. Но она отлично тренирует: измерение времени (time.Since) + периодические события (Ticker).
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
fmt.Println("working, elapsed:", time.Since(start))
}
}
Если вы запустите это, увидите, что “elapsed” растёт, а тики приходят регулярно.
Небольшой факт “из мира Go”: начиная с Go 1.9, пакет time прозрачно отслеживает монотонную составляющую времени в значениях time.Time, и поэтому time.Since(start) остаётся корректным даже при сдвигах системных часов (в рамках разумного). Это не меняет ваш код, но делает измерения надёжнее.
Почему тикер не идеально точный
Очень хочется думать, что тикер — это швейцарские часы. Но он скорее “хороший будильник”, который может прозвонить чуть позже, если программа занята или ОС решила, что сейчас важнее заняться чем-то другим.
Практически это означает: тикер — это регулярный сигнал, а не гарантия микросекундной точности. Если вам нужна точность уровня “каждые 10ms строго”, то вы уже заходите в область, где придётся обсуждать нагрузку, планировщик, приоритеты — и вообще становиться немножко грустным. Мы туда не идём.
4. Как выбрать: Sleep, After, Ticker
Когда новичок видит три инструмента, он часто выбирает случайно. Чтобы не гадать, полезно держать в голове простую модель: что одноразовое, что периодическое, и что требует остановки.
Вот небольшая таблица, которая помогает “разложить по полочкам”:
| Инструмент | Что делает по смыслу | Сколько событий | Нужно ли останавливать |
|---|---|---|---|
|
“Пауза в текущем коде” | 0 (просто пауза) | нет |
|
“Событие через d” | 1 | нет (в вашем коде) |
|
“Событие каждые d” | много | да, ticker.Stop() |
Почему Sleep не равно After, хотя оба “ждут”? Потому что After возвращает событие (канал), и его проще комбинировать с другими ожиданиями, когда вы дойдёте до более сложных конструкций. Сегодня мы это не используем, но стиль уже закладываем.
5. Учебное приложение: ожидание и периодический статус
Теперь давайте сделаем шаг от “просто тикает” к маленькой утилите, где таймеры и тикеры выглядят как часть поведения программы.
Представим, что наше консольное приложение умеет “ждать старт”, а потом “печатать статус”. Это может напоминать мини‑таймер для фокус‑сессии: сначала ждём начало, потом каждые N секунд печатаем, что мы ещё живы.
Сделаем две маленькие функции, чтобы код читался проще. Мы умеем писать функции уже давно, просто наконец используем это в быту.
Функция: подождать старт через time.After
package main
import (
"fmt"
"time"
)
func waitStart(delay time.Duration) {
fmt.Println("waiting:", delay) // waiting: 300ms
<-time.After(delay)
fmt.Println("go!") // go!
}
func main() {
waitStart(300 * time.Millisecond)
}
Функция: печатать статус по тикеру N раз
package main
import (
"fmt"
"time"
)
func printStatusEvery(d time.Duration, times int) {
ticker := time.NewTicker(d)
defer ticker.Stop()
for i := 1; i <= times; i++ {
<-ticker.C
fmt.Println("status tick:", i) // status tick: 1 ...
}
}
func main() {
printStatusEvery(200*time.Millisecond, 3)
}
Собираем вместе: старт через 0.5s, потом 5 статусов
package main
import (
"time"
)
func main() {
waitStart(500 * time.Millisecond)
printStatusEvery(300*time.Millisecond, 5)
}
Да, тут waitStart и printStatusEvery должны быть в том же файле (или импортированы из пакета, но пакеты мы сейчас не усложняем). Смысл в том, что примеры начинают складываться в одну историю: “мы умеем ждать событие” и “мы умеем делать действие раз в интервал”.
Граница темы: чего мы сознательно не делаем сегодня
Очень легко “случайно” упасть в более сложный уровень. Например, вы можете захотеть: “а можно сделать так, чтобы тикер тикал, но я мог его остановить по условию раньше?” — и тут почти неизбежно возникнут конкурентность и select.
Сегодня мы не используем select, не запускаем горутины, не комбинируем ожидание тикера с ожиданием чего-то ещё. У нас модель простая: пришли к ожиданию → заблокировались → получили событие → пошли дальше. Для начала это идеально: вы понимаете механику и не утопаете в новых сущностях.
6. Типичные ошибки при работе с time.After и time.NewTicker
Ошибка №1: забыть, что time.After и ticker.C — это “сигнал”, а не “функция паузы”.
Иногда студент пишет time.After(1 * time.Second) и ждёт, что программа подождёт. Но без чтения <- вы просто создали таймер и выбросили его “в космос”, а код пошёл дальше. Правильная форма ожидания выглядит как <-time.After(d) или чтение из сохранённого канала.
Ошибка №2: создавать тикер и не останавливать его.
Тикер — это объект, который живёт и генерирует события. Если вы его не остановили, он будет продолжать “тикать”, даже если вам уже не нужен. В короткой программе это может быть не заметно, но в долгоживущем приложении это превращается в утечку ресурсов и странные фоновые активности. Привычка простая: создали ticker := time.NewTicker(...) — сразу следующей строкой мысленно добавили defer ticker.Stop().
Ошибка №3: делать time.After внутри частого цикла без понимания последствий.
Паттерн вида “в каждом обороте цикла делаю <-time.After(10ms)” выглядит невинно, но он каждый раз создаёт новый таймер. Иногда это нормально, но часто правильнее использовать один тикер. Если вам нужен регулярный ритм — тикер обычно понятнее и дешевле.
Ошибка №4: ожидать идеальной периодичности и нулевого дрейфа.
Если вы печатаете сообщение “каждую секунду”, это значит “примерно каждую секунду”. Под нагрузкой, при медленном выводе в консоль, при фоновых задачах ОС тики могут приходить чуть позже. Это не “ошибка Go”, это реальность исполнения программ. Воспринимайте тикер как регулярный сигнал, а не как лабораторный генератор импульсов.
Ошибка №5: пытаться на таймерах построить таймаут операции, не меняя контракт функций.
Иногда хочется: “я поставлю time.After, и если операция не успела — всё отменится”. Но само по себе ожидание таймера ничего не отменяет: оно только говорит “время вышло”. Чтобы операция реально прекращалась, нужен договор в коде (обычно через context и проверку отмены). Это отдельная тема: не путайте “ждать событие времени” и “уметь отменять работу”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ