JavaRush /Курси /Go SELF /Таймери й тікери — time.After, time.NewTicker

Таймери й тікери — time.After, time.NewTicker

Go SELF
Рівень 34 , Лекція 3
Відкрита

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

Коли новачок бачить три інструменти, він часто вибирає навмання. Щоб не гадати, корисно тримати в голові просту модель: що одноразове, що періодичне і що потребує зупинки.

Ось невелика таблиця, яка допомагає розкласти все по поличках:

Інструмент Що це означає Скільки подій Чи потрібно зупиняти
time.Sleep(d)
«Пауза в поточному коді» 0 (просто пауза) ні
time.After(d)
«Подія через d» 1 ні (у вашому коді)
time.NewTicker(d)
«Подія кожні 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 і перевірку скасування. Це окрема тема: не плутайте «чекати подію часу» і «вміти скасовувати роботу».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ