хватайте -30% на все до 18 ноября
до 18 ноября
Хватай скидки -30% на все!
Черная пятница

Контекст в Go: как задавать время выполнения операции и отменять операции

Автор — Владимир Балун
ex-TeamLead в Яндекс
Содержание
Когда приложение работает с сетевыми запросами, базой данных или несколькими горутинами, не всегда можно заранее знать, сколько времени займёт операция. Если не предусмотреть отмену и ограничения по времени, часть задач может продолжать выполняться даже тогда, когда результат уже не нужен.

В этой статье разберёмся, что такое context в Go, где он используется и как с его помощью задавать таймауты, дедлайны, отменять операции и передавать значения между функциями.

Что такое Golang context?

Контекст в программировании помогает управлять выполнением операций:
  • временем выполнения операций;
  • отменой запущенных процессов;
  • установленными дедлайнами;
  • передачей данных, связанных с конкретным запросом, например ID пользователя или запроса.
Например, приложение отправляет запрос к внешнему сервису. Если тот долго не отвечает, ждать бесконечно нет смысла. Контекст позволяет заранее установить ограничение по времени или в нужный момент отправить сигнал об отмене.

Контекст особенно полезен там, где операция может выполняться долго или зависит от внешних ресурсов. Чаще всего с ним можно встретиться в трех задачах:
  • сетевые запросы — например, при обращении к API, когда контекст позволяет задать таймаут и прекратить ожидание, если сервис слишком долго не отвечает;
  • работа с базой данных — запрос можно ограничить по времени или отменить, если он больше не нужен, что помогает не удерживать соединения и другие ресурсы дольше необходимого;
  • фоновые горутины — контекст позволяет передать им сигнал о завершении работы приложения, поэтому горутина может получить этот сигнал и корректно завершиться, вместо того чтобы продолжать работу после остановки сервиса.

Приёмы работы с контекстом

Создание контекста с таймаутом и дедлайном

WithTimeout() пригодится, когда операция не должна продолжаться дольше определённого времени.

Например, приложение обращается к стороннему API. Мы готовы ждать ответа максимум 15 секунд. Если ответа нет, запрос нужно прекратить.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
Функция возвращает две вещи: сам контекст и функцию cancel, с помощью которой его можно отменить (до таймаута, в случае необходимости).

Строка defer  cancel()  гарантирует, что отмена будет выполнена после завершения текущей функции. Это хорошая привычка при создании контекстов с таймерами: даже если операция закончилась раньше установленного срока, связанные с контекстом ресурсы можно освободить сразу.

WithDeadline() решает похожую задачу, но вместо продолжительности операции мы указываем конкретное время окончания.
ctx, cancel := context.WithDeadline(
    context.Background(),
    time.Now().Add(10*time.Second),
)
defer cancel()
По смыслу разница между двумя функциями выглядит так:
  • если известна именно продолжительность, обычно удобнее использовать WithTimeout(), которая означает, что «операция может выполняться ещё 10 секунд»;
  • если важен конкретный момент времени, то подходит WithDeadline(), которая означает, что «операция должна закончиться до определённого времени».
При этом эти механизмы тесно связаны: таймаут фактически можно представить как дедлайн, рассчитанный относительно текущего времени.

Отмена вручную

Не всегда нужно ждать, пока закончится таймер. Иногда работа становится ненужной раньше, и программу нужно уведомить об этом самостоятельно. Для таких случаев существует context.WithCancel().

Допустим, приложение отправляет несколько параллельных запросов к разным сервисам в поисках одного результата. Как только один из них ответил подходящим вариантом, остальные запросы можно остановить.
ctx, cancel := context.WithCancel(context.Background())
go func() {
    time.Sleep(time.Second)
    cancel() 
}()
Такой подход особенно полезен при параллельной работе. Например, несколько горутин могут одновременно обращаться к разным источникам данных. Если результат уже найден, нет необходимости продолжать остальные операции и тратить ресурсы.

Передача значений

Помимо отмены и ограничения времени, контекст может передавать значения между связанными функциями. Для этого используется context.WithValue().
ctx = context.WithValue(ctx, "requestID", "abc123")
Однако здесь важно не увлечься. context.WithValue() не предназначен для передачи любых параметров между функциями. WithValue() лучше использовать для данных, которые относятся именно к текущему запросу и должны пройти через несколько уровней приложения.

Корневые контексты Background и TODO

context.Background() подходит как корневой контекст, когда выше по цепочке ещё нет контекста. Например, он может использоваться на верхнем уровне приложения.

context.TODO() — скорее временная отметка: здесь контекст нужен, но пока непонятно, какой именно. В готовом коде лучше постепенно заменить такие места на подходящий контекст.

Заключение

Контекст в Go помогает контролировать выполнение операций и не оставлять программу ждать там, где это уже не имеет смысла. С его помощью можно задавать ограничения по времени, отменять ненужную работу, управлять фоновыми горутинами и передавать данные, связанные с конкретным запросом.

При этом важно использовать контекст по назначению: не превращать его в хранилище обычных параметров и не забывать корректно завершать созданные контексты. Чем раньше вы разберетесь с этими принципами, тем проще будет писать устойчивые и предсказуемые приложения на Go.

Глубокий Go — курс о нюансах и тонкостях языка

Подойдёт, если хочешь детально разобраться в работе строк, срезов, словарей и других особенностях языка Go
Другие статьи