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)
Це читається майже як людська мова: «почекати, доки не мине 200 мс».
Для візуального уявлення нехай буде маленька схема:
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("початок") // початок
<-time.After(200 * time.Millisecond) // чекаємо подію часу
fmt.Println("готово") // готово
}
Тут важливо, що <-time.After(...) блокує виконання так само, як Sleep, але психологічно ви звикаєте до патерна «я чекаю сигнал».
Таймер повертає time.Time: можна побачити, коли саме він спрацював
Подія часу — це не просто «пінг». На канал надходить момент часу, коли таймер спрацював, тобто значення типу time.Time. Це інколи зручно для логів і діагностики.
package main
import (
"fmt"
"time"
)
func main() {
firedAt := <-time.After(100 * time.Millisecond)
fmt.Println("таймер спрацював о:", firedAt.Format(time.RFC3339Nano))
}
Ми не заглиблюємося у форматування — це окрема тема. Але приємно розуміти: таймер — не «магія», а цілком конкретний механізм.
Дочекатися дедлайну: time.Until + time.After
Часта задача: у нас є конкретний дедлайн, і ми хочемо дочекатися його. Для цього зручно обчислити тривалість «скільки залишилося» й почекати її.
package main
import (
"fmt"
"time"
)
func main() {
deadline := time.Now().Add(150 * time.Millisecond)
<-time.After(time.Until(deadline))
fmt.Println("дедлайн настав") // дедлайн настав
}
Якщо дедлайн уже минув, 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("тік", i) // тік 1, тік 2, тік 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("працюємо, минуло:", time.Since(start))
}
}
Якщо ви запустите це, побачите, що «elapsed» зростає, а тики приходять регулярно.
Невеликий факт із Go: починаючи з Go 1.9, пакет time прозоро відстежує монотонну складову часу у значеннях time.Time. Тому time.Since(start) залишається коректним навіть під час зсувів системного часу — у межах розумного. Це не змінює ваш код, але робить вимірювання надійнішими.
Чому тікер не ідеально точний
Дуже хочеться думати, що тікер — це швейцарський годинник. Але він радше «хороший будильник», який може продзвонити трохи пізніше, якщо програма зайнята або ОС вирішила, що зараз важливіше зайнятися чимось іншим.
Практично це означає: тікер — це регулярний сигнал, а не гарантія мікросекундної точності. Якщо вам потрібна точність на рівні «кожні 10 мс чітко», ви вже заходите в область, де доведеться говорити про навантаження, планувальник і пріоритети. І це вже зовсім інший рівень складності.
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("очікуємо:", delay) // очікуємо: 300ms
<-time.After(delay)
fmt.Println("рушили!") // рушили!
}
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,5 с, потім 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 і перевірку скасування. Це окрема тема: не плутайте «чекати подію часу» і «вміти скасовувати роботу».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ