time.Duration и арифметика времени в Go: Add, Sub, Since и ParseDuration

В пакете time есть два разных понятия. time.Time как конкретный момент времени. time.Duration как промежуток между моментами. Форматирование и парсинг строк я уже разбирал в статье «Форматирование дат и времени в Go». Здесь сосредоточимся на другом: как складывать и вычитать время, измерять интервалы и работать с длительностями.

Это один из разборов в серии для начинающих. Общий маршрут изучения языка собран в статье «Go для начинающих: дорожная карта».

Что такое time.Duration

time.Duration внутри себя это один int64 в котором хранится количество наносекунд. Поэтому длительности можно складывать, вычитать и умножать на число как обычные числа.

package main

import (
	"fmt"
	"time"
)

func main() {
	d := 2*time.Hour + 30*time.Minute
	fmt.Println(d) // 2h30m0s

	fmt.Println(d * 2) // 5h0m0s
	fmt.Println(d / 3) // 50m0s
}

Константы time.Hour, time.Minute, time.Second, time.Millisecond и так далее — это значения Duration. Чтобы получить «3 секунды», нужно писать 3 * time.Second, а не просто 3:

timeout := 3 * time.Second // правильно
wrong := 3                 // это 3 наносекунды, а не 3 секунды

Метод String() (вызывается автоматически при печати) выводит длительность в человекочитаемом виде: 2h3m45s.

Перевод в числа

У Duration есть методы для перевода в конкретные единицы. Для разных единиц измерения тоже разные: Hours, Minutes и Seconds возвращают float64, а Milliseconds, Microseconds и Nanoseconds уже возвращают int64.

d := 90 * time.Minute

fmt.Println(d.Hours())        // 1.5      (float64)
fmt.Println(d.Minutes())      // 90       (float64)
fmt.Println(d.Milliseconds()) // 5400000  (int64)

Если нужно собрать время формата HH:MM:SS вручную, делите с остатком:

d := 1*time.Hour + 14*time.Minute + 30*time.Second
h := int(d.Hours())
m := int(d.Minutes()) % 60
s := int(d.Seconds()) % 60
fmt.Printf("%02d:%02d:%02d\n", h, m, s) // 01:14:30

Арифметика моментов

С самим time.Time работают три метода.

Add сдвигает момент на некую длительность:

now := time.Now()
later := now.Add(15 * time.Minute) // через 15 минут
before := now.Add(-1 * time.Hour)  // час назад

Sub возвращает разницу между двумя моментами как Duration:

start := time.Now()
// ... работа ...
elapsed := time.Now().Sub(start)
fmt.Println(elapsed) // 1.2s

AddDate сдвигает по календарю на годы, месяцы и дни. Это не то же самое, что прибавить 24 * time.Hour, потому что AddDate учитывает разную длину месяцев и переход на летнее время.

t := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
fmt.Println(t.AddDate(0, 1, 0).Format("2006-01-02"))
// 2026-03-03

31 января плюс месяц даёт 3 марта, а не несуществующую дату в феврале: лишние дни нормализуются. Это поведение по умолчанию, его стоит держать в голове при расчёте сроков подписок и подобных задач.

time.Since и time.Until

Две частые операции, «сколько прошло» и «сколько осталось», оформлены отдельными функциями. Они короче, чем ручной Sub двух дат.

start := time.Now()
doWork()
fmt.Println("заняло:", time.Since(start)) // равно time.Now().Sub(start)

deadline := time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC)
fmt.Println("осталось:", time.Until(deadline)) // равно deadline.Sub(time.Now())

time.Since(start) — широко используется для замера время исполнения участка кода.

Парсинг длительностей: ParseDuration

Если длительность приходит строкой (например, из конфига или флага командной строки), её разбирает time.ParseDuration:

d, err := time.ParseDuration("1h30m")
if err != nil {
	// обработка ошибки
}
fmt.Println(d) // 1h30m0s

Допустимые единицы: ns, us (или µs), ms, s, m, h. Единицы для дней нет — "1d" приведёт к ошибке:

_, err := time.ParseDuration("1d")
fmt.Println(err)
// time: unknown unit "d" in duration "1d"

Для суток пишите "24h". Строку можно делать дробной и знаковой: "-1.5h", "300ms", "2h45m".

Округление длительностей

Round округляет к ближайшему кратному, Truncate отбрасывает остаток вниз:

d := 1*time.Hour + 14*time.Minute + 30*time.Second

fmt.Println(d.Round(time.Hour))    // 1h0m0s
fmt.Println(d.Truncate(time.Hour)) // 1h0m0s
fmt.Println(d.Round(time.Minute))  // 1h15m0s (30s округлились вверх)

Те же методы есть и у time.Time. Они удобны, когда нужно «обнулить» секунды или привести момент к началу часа.

Сравнение моментов

Для сравнений time.Time есть методы Before, After и Equal. Оператором == сравнивать моменты не стоит: он учитывает не только сам момент, но и часовой пояс с монотонными часами, поэтому два «одинаковых» времени могут оказаться не равны.

if deadline.Before(time.Now()) {
	fmt.Println("дедлайн прошёл")
}

// для проверки равенства момента — Equal, не ==
if t1.Equal(t2) {
	fmt.Println("один и тот же момент")
}

Заключение

Две главные составляющие пакета time это момент времени (time.Time) и промежуток (time.Duration). Длительность под капотом представляет собой int64 наносекунд, поэтому её можно складывать и умножать как число, но константы вроде time.Second обязательны для использования: «голое» число 3 выразится как три наносекунды.

Для большинства работы хватает:

  1. Add и Sub — сдвиг момента и разница между моментами.
  2. AddDate — календарный сдвиг с учётом длины месяцев.
  3. time.Since и time.Until — «сколько прошло» и «сколько осталось».
  4. ParseDuration — разбор строк вроде "1h30m" (без единицы для дней).

Форматирование и парсинг дат и времени из строк — отдельная тема, она разобрана в статье про reference time 2006-01-02.


Теги: