JavaRush /Курси /Go SELF /time.Since, Add/Sub, порівняння часу, UTC та Local

time.Since, Add/Sub, порівняння часу, UTC та Local

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

1. Навіщо порівнювати час і рахувати, скільки минуло

Коли ви вперше бачите time.Time, здається, що це «просто ще один тип, який можна надрукувати». Але майже будь-яка реальна програма рано чи пізно зводиться до запитань на кшталт: «чи встигли ми в дедлайн?», «скільки зайняла операція?», «коли треба показати користувачу нагадування?», «запис застарів чи ще свіжий?». У такі моменти вам потрібна не краса формату дати, а арифметика й порівняння.

Важливо розуміти, що в Go розробники стандартної бібліотеки свідомо зробили API таким, щоб ви менше помилялися. Тому замість ручних порівнянь через > і < є методи Before/After/Equal. Замість ручного time.Now().Sub(start) є читабельніший time.Since(start). Це не просто «синтаксичний цукор», а спосіб зробити код зрозумілішим і безпечнішим — і для вас через тиждень, і для колеги, який відкриє ваш файл через рік. І так, якщо там усе в мілісекундах без підписів, він тихо плакатиме.

2. Add і Sub: «момент ± тривалість» і «момент − момент»

Почнімо з найпростішої арифметики часу. У Go є дуже просте правило, яке корисно тримати в голові як таблицю множення:

  • Time + Duration = Time (зсуваємо момент уперед або назад)
  • Time - Time = Duration (отримуємо, скільки часу минуло між двома моментами)
  • Duration + Duration = Duration (складаємо інтервали)

Напряму додавати два time.Time не можна — і це добре: менше шансів випадково написати нісенітницю.

Додаємо тривалість: t.Add(d)

Після кожного підзаголовка я коротко підводитиму вас до наступної думки. Тут підводка проста: часто ви знаєте, коли щось почалося, і знаєте, скільки має тривати, — отже, хочете отримати момент завершення.

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Date(2026, 1, 16, 10, 0, 0, 0, time.UTC)
	end := start.Add(45 * time.Minute)

	fmt.Println("start:", start) // start: 2026-01-16 10:00:00 +0000 UTC
	fmt.Println("end:  ", end)   // end:   2026-01-16 10:45:00 +0000 UTC
}

Зверніть увагу на дві речі. По-перше, Add не змінює змінну start, а повертає новий time.Time. По-друге, 45 * time.Minute виглядає як «людська» тривалість — не треба пам’ятати, скільки це в секундах.

Різниця моментів: end.Sub(start)

Тепер симетрична історія: часто в нас є початок і кінець, і ми хочемо тривалість. Це «ідеальний» випадок для Sub.

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Date(2026, 1, 16, 10, 0, 0, 0, time.UTC)
	end := time.Date(2026, 1, 16, 10, 45, 0, 0, time.UTC)

	elapsed := end.Sub(start)

	fmt.Println("elapsed:", elapsed) // elapsed: 45m0s
}

Якщо ви колись побачите в коді «ручні» обчислення типу (endUnix - startUnix) / 60, знайте: поруч, імовірно, ховається баг, який чекає свого часу. Sub не ідеальний для всіх сценаріїв у світі — у часу є календарні нюанси, — але для запитання «скільки між двома моментами» це саме те, що потрібно.

3. time.Since(start) і time.Until(deadline)

Підводка тут така: часто другий момент нам навіть не потрібен як окрема змінна. Ми хочемо дізнатися, скільки минуло від старту до «зараз», або скільки залишилося до дедлайна від «зараз». Можна, звісно, щоразу писати time.Now().Sub(start), але Go пропонує функції, які читаються як звичайна англійська. І це рідкісний випадок, коли англійська в коді справді допомагає.

Вимірюємо, скільки зайняло: time.Since(start)

time.Since(start) — це, по суті, time.Now().Sub(start), але намір читається миттєво: «since start».

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()

	time.Sleep(30 * time.Millisecond)

	fmt.Println("since:", time.Since(start)) // приклад: since: 30.1ms
}

Важливий практичний момент: time.Now() краще викликати один раз, зберегти в start і далі працювати від нього. Якщо ви зробите два виклики time.Now() «ніби поруч», то все одно отримаєте різні значення. Іноді це ламає логіку в несподіваних місцях, особливо коли порівняння відбуваються «на межі».

Скільки залишилося до дедлайна: time.Until(deadline)

Якщо Since — це «минуло», то Until — це «залишилося».

package main

import (
	"fmt"
	"time"
)

func main() {
	deadline := time.Now().Add(200 * time.Millisecond)

	fmt.Println("left:", time.Until(deadline) > 0) // left: true (у типовому запуску)
}

time.Until(deadline) може бути відʼємним. І це не помилка: це чесна відповідь «дедлайн уже минув». Тому якщо ви хочете перевірити, чи встигли, зазвичай порівнюють із нулем (> 0 або <= 0), або ж порівнюють моменти через Before/After — про них далі.

4. Порівняння часу: Before, After, Equal

Порівняння часу — це та частина, де початківці найчастіше намагаються вигадати свій велосипед: порівняти рядки, порівняти окремо години, окремо хвилини або взагалі перевести все в числа й жити, ніби ми досі в кам’яному віці. Насправді time.Time — це багатий тип, і в нього є методи, які виражають намір краще, ніж будь-які «ручні» хаки.

Перевіряємо порядок: t1.Before(t2) і t1.After(t2)

Уявіть, що ви пишете перевірку «початок раніше кінця». У нормальному світі це має бути один рядок, а не мініалгоритм на 20 рядків із перевірками на кшталт «якщо рік дорівнює».

package main

import (
	"fmt"
	"time"
)

func main() {
	t1 := time.Date(2026, 1, 16, 10, 0, 0, 0, time.UTC)
	t2 := t1.Add(1 * time.Hour)

	fmt.Println(t1.Before(t2)) // true
	fmt.Println(t2.After(t1))  // true
}

Код читається майже як українське речення: «t1 before t2». Це і є мета.

Рівність: Equal і чому == — не завжди те, чого ви хочете

У time.Time є метод Equal, і його варто використовувати, коли ви порівнюєте моменти як один і той самий момент на шкалі часу. Чому не просто t1 == t2? Тому що в time.Time є нюанси внутрішнього представлення, зокрема пов’язані з монотонною складовою, і порівняння як структур може поводитися інакше, ніж порівняння як моментів.

Поки ви новачок, добре правило таке: для порівняння моментів використовуйте Before/After/Equal. Це робить намір явним, і ви менше залежите від внутрішніх деталей.

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Date(2026, 1, 16, 10, 0, 0, 0, time.UTC)

	fmt.Println(t.Equal(t)) // true
}

5. UTC та Local: один момент, багато відображень

Ось тут зазвичай настає момент — перепрошую за каламбур, — коли людям починає здаватися, що час змовився проти них. І, чесно кажучи, вони не зовсім помиляються.

Пояснення таке: у програмі є подія — наприклад, зустріч о 10:00 UTC. Це один і той самий момент на часовій шкалі. Але якщо ви перебуваєте в іншій зоні, цей момент виглядатиме як інший локальний час. Тобто момент не змінюється, змінюється лише те, як ми його показуємо.

UTC(), Local() і In(loc) — це про представлення

Найчастіша помилка новачка — думати, що t.UTC() «переводить час» і «зсуває подію». Ні. Він змінює лише часову зону представлення для того самого моменту.

Подивімося на приклад із фіксованою зоною — так простіше й передбачуваніше для навчання.

package main

import (
	"fmt"
	"time"
)

func main() {
	utc := time.Date(2026, 1, 16, 12, 0, 0, 0, time.UTC)
	loc := time.FixedZone("UTC+3", 3*60*60)

	inLoc := utc.In(loc)

	fmt.Println("utc:  ", utc)                 // utc:   2026-01-16 12:00:00 +0000 UTC
	fmt.Println("loc:  ", inLoc)               // loc:   2026-01-16 15:00:00 +0300 UTC+3
	fmt.Println("equal:", utc.Equal(inLoc))    // equal: true
}

Останній рядок — найважливіший. Equal каже: так, це один і той самий момент. Просто в одній зоні він «12:00», а в іншій — «15:00». Як у житті: одна й та сама онлайн-зустріч, але хтось підключається «о 9-й ранку», а хтось — «о 18-й».

Чому в програмах люблять UTC

Про UTC можна сказати без пафосу: це зручна «універсальна лінійка». Якщо ви зберігаєте часи в UTC, вам простіше їх порівнювати, менше сюрпризів і менша залежність від оточення: сервер в одному місці, користувач — в іншому, тести запускаються в третьому. А локальну зону частіше використовують на межі — коли потрібно показати людині час так, як вона звикла його бачити.

При цьому ви не зобов’язані «все робити в UTC завжди». Головне — розуміти: зона є лише відображенням, а порівняння й арифметика працюють із моментами.

6. Мінітаймтрекер: вимір і дедлайн

Щоб тема не лишалася абстрактною, давайте додамо в наш навчальний застосунок маленьку функціональність: вимір тривалості з планом. Нехай програма робить таке: «зараз стартуємо», «план — N хвилин», «через деякий час перевіряємо, чи прострочили ми дедлайн», а потім друкує зрозумілий вивід.

Це не повноцінний продукт — світ поки не готовий, — але це хороша навчальна цеглинка: тут є Add, Since, порівняння часу й акуратний вивід.

Модель даних: Session

Почнемо з простої структури. Ми вже вміємо працювати зі структурами, тож використаємо їх, а не розрізнені змінні.

package main

import "time"

type Session struct {
	StartedAt time.Time
	Planned   time.Duration
}

StartedAt — це time.Time, а Plannedtime.Duration. Ці поля неможливо випадково переплутати місцями: типи різні, компілятор не дозволить.

Дедлайн сесії: Deadline()

Тепер додамо метод, який повертає момент дедлайна. Тут прямо використовується правило «момент + тривалість = момент».

package main

import "time"

type Session struct {
	StartedAt time.Time
	Planned   time.Duration
}

func (s Session) Deadline() time.Time {
	return s.StartedAt.Add(s.Planned)
}

Метод маленький, але дуже важливий: він ховає «як ми рахуємо дедлайн» в одному місці. Якщо ви потім вирішите, що план треба округлювати або враховувати перерви, змінюватимете це тут, а не по всьому коду.

Перевірка «чи прострочили»: IsOverdue(now)

Підводка така: порівняння майже завжди читаються краще, якщо винести їх у маленьку функцію або метод із промовистою назвою. Тоді основний код стає схожим на текст.

package main

import "time"

func (s Session) IsOverdue(now time.Time) bool {
	return now.After(s.Deadline())
}

Ми навмисно приймаємо now як параметр, а не викликаємо time.Now() всередині. Це робить код передбачуванішим і, у майбутньому, легшим для тестування: можна підставити будь-який «поточний момент».

Мініmain: стартуємо, «працюємо», перевіряємо

Тепер склеїмо все в коротку програму. Так, ми використовуємо time.Sleep просто як імітацію перебігу часу, — це нормально для демонстрації.

package main

import (
	"fmt"
	"time"
)

type Session struct {
	StartedAt time.Time
	Planned   time.Duration
}

func (s Session) Deadline() time.Time { return s.StartedAt.Add(s.Planned) }
func (s Session) IsOverdue(now time.Time) bool {
	return now.After(s.Deadline())
}

func main() {
	s := Session{StartedAt: time.Now(), Planned: 50 * time.Millisecond}
	time.Sleep(80 * time.Millisecond)

	fmt.Println("elapsed:", time.Since(s.StartedAt)) // приклад: elapsed: 80.2ms
	fmt.Println("overdue:", s.IsOverdue(time.Now())) // overdue: true
}

Тут видно одразу кілька речей. Ми використовуємо time.Since для «скільки минуло», Add схований у Deadline, а порівняння виконується через After, що читається по-людськи. Це вже «скелет» таймтрекера, до якого пізніше можна додати форматування виводу та введення параметрів.

Маленька схема: як ці операції пов’язані

Іноді корисно «побачити» зв’язки між типами. Давайте зафіксуємо це простою схемою:

flowchart LR
    A[time.Time: початок] -- Add(time.Duration) --> B[time.Time: дедлайн]
    C[time.Time: зараз] -- Sub(початок) --> D[time.Duration: минуло]
    C -- After(дедлайн) --> E[bool: прострочено?]

Саме так це зазвичай і працює в застосунках: є старт, є дедлайн (початок + запланований час), є «поточний час», є «минуло» (зараз - початок), і є логічне рішення «прострочено?».

7. Типові помилки

Помилка № 1: вимірювати тривалість як time.Now().Sub(time.Now()).
Так іноді пишуть «в один рядок», але це логічна помилка: два виклики time.Now() дають два різні моменти, і різниця може бути мікроскопічною, шумною або взагалі не відображати те, що ви хотіли виміряти. Правильний підхід — зберегти start := time.Now() один раз, виконати роботу, а потім використати time.Since(start) або time.Now().Sub(start).

Помилка № 2: плутати «момент» і «тривалість» та намагатися додавати або порівнювати їх як завгодно.
Якщо ви відчуваєте бажання «додати два часи», зупиніться й запитайте себе: ви точно хочете момент, а не тривалість? У коректній моделі «складають» лише тривалості, а момент можна тільки зсувати на тривалість: t.Add(d).

Помилка № 3: порівнювати часи через рядки або вручну порівнювати години й хвилини.
Це одна з найдорожчих за наслідками помилок. Рядки залежать від формату, локалі та зони, а ручні порівняння легко ламаються на межі дати, місяця чи року. У Go для порівняння моментів є Before/After/Equal, і їх варто використовувати за замовчуванням, бо вони виражають намір і захищають від саморобної логіки.

Помилка № 4: думати, що t.UTC() «зсуває подію в часі».
UTC() і In(loc) змінюють відображення, а не момент. Якщо ви «перевели» 12:00 UTC у UTC+3, ви побачите 15:00, але це той самий момент. Тому перевіряйте себе запитанням: «Я хочу змінити відображення для людини чи справді змінити дедлайн або розклад?» Майже завжди вам потрібне перше.

Помилка № 5: не враховувати, що time.Until(deadline) може бути відʼємним.
Якщо ви пишете логіку «якщо залишилося менше хвилини — попереджаємо», пам’ятайте, що «залишилося» може стати «мінус 3 год». Це не баг Until, а чесна відповідь. Правильна обробка — явно перевіряти знак або порівнювати моменти (now.After(deadline)), щоб код поводився передбачувано і після дедлайна також.

1
Задача
Go SELF, 34 рівень, 1 лекція
Недоступна
Перерахунок рейсу
Перерахунок рейсу
1
Задача
Go SELF, 34 рівень, 1 лекція
Недоступна
Звіт маршруту
Звіт маршруту
1
Задача
Go SELF, 34 рівень, 1 лекція
Недоступна
Вимірювання завантаження
Вимірювання завантаження
1
Задача
Go SELF, 34 рівень, 1 лекція
Недоступна
Одна подія — різні зони
Одна подія — різні зони
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ