Context управляет отменой и дедлайнами в Go, позволяя корректно завершать горутины и запросы. Разберу практические шаблоны передачи context через middleware и типичные ошибки с конкретными примерами и кодом.
Context управляет отменой и дедлайнами в Go и нужен для безопасного завершения операций и освободения ресурсов. Ниже — практическое руководство с рабочими примерами на 2025–2026 годы и конкретными числами для таймаутов и тестов.
Шаг 1: Что сделать прежде чем использовать context
Что такое context
Context — это стандартный пакет в Go (package context), введённый в 2017 году и теперь обязательный инструмент для управления lifetime операций: отмена (cancellation), дедлайны (deadline) и передача метаданных с помощью ключ-значение. Контекст не предназначен для хранения больших структур или конфигураций; он лёгкий, immutable по дизайну и безопасен для конкурентного доступа.
Практическое правило: используйте context для:
прерывания длительных запросов (HTTP, gRPC, DB) по таймауту или отмене;
передачи request-scoped значений (request id, trace id) — не конфигурации;
ограничения времени выполнения с точностью до миллисекунд (пример: 5000ms = 5s).
0
Статья была полезной?
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…
Схема работы context: parent -> child -> cancel
Шаг 2: Настроить базовый шаблон использования
Передавать context нужно первым параметром функции: func Do(ctx context.Context, ...) error. Это фиксированное соглашение, которое соблюдается в стандартной библиотеке и в популярных репозиториях.
Пояснение: здесь context управляет HTTP-клиентом. Если ctx будет отменён (cancel), http.DefaultClient прекратит запрос и вернёт ошибку. Для числовых примеров используйте тайм-ауты 3s, 5s, 30s в зависимости от ожиданий SLA.
Шаг 3: Context.WithCancel и WithTimeout — практические примеры
Context.WithCancel и WithTimeout
Существует три стандартных производителя контекста: WithCancel, WithTimeout, WithDeadline. На практике чаще нужны WithCancel и WithTimeout.
WithCancel возвращает (ctx, cancel). Вызывать cancel() обязательно, даже если вам не нужно экстренно прерывать — для освобождения ресурсов.
WithTimeout(ctx, d) автоматически вызовет cancel после d времени. Тем не менее паттерн defer cancel() всё равно обязателен.
// пример: одновременная работа с timeout и ручной отменой
func Process(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // 5s — конкретика
defer cancel()
done := make(chan struct{})
go func() {
// длительная операция, возможна отмена
time.Sleep(3 * time.Second)
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err() // context.DeadlineExceeded или context.Canceled
}
}
Числа: 5 секунд — распространённый таймаут для внутренних HTTP-запросов в backend, 30s — для внешних интеграций с нестабильными API. В тестах используйте 100–500ms для быстрых unit тестов, как показано в шаге 5.
Пример использования Context.WithTimeout в коде Go
Шаг 4: Как передавать context через middleware
Как передавать context через middleware
В HTTP-серверах на базе net/http и в фреймворках (chi, gorilla/mux) контекст приходит в request.Context(). В middleware можно создать дочерний контекст с таймаутом, добавить значения и передать дальше через r = r.WithContext(ctx).
Практика: используйте 5–10 секунд для пользовательских запросов, 0.5–2 секунды для коротких внутренних вызовов. Никогда не храните *context.Context в глобальных переменных или в struct field для длительного хранения — это приведёт к утечкам и неверной передаче scope.
Не ставьте таймауты в middleware короче, чем у downstream сервисов, иначе upstream обрежет запрос раньше, чем он завершится.
Скопируйте request-id в лог при каждом роге, чтобы упростить трассировку.
Шаг 5: Типичные ошибки
Типичные ошибки
Опишу ошибки, которые я видел в реальных кодовых базах в 2025–2026 годах, с числами и решениями.
Забыли вызвать cancel(): при WithCancel/WithTimeout всегда defer cancel(). Без этого остаются неиспользуемые ресурсовые объекты и могут накапливаться goroutine: на проекте среднего размера это приводит к росту памяти на 20–50% за 24 часа.
Передача context как struct field: контекст должен передаваться в аргументах. Хранить его в структуре для повторного использования — категорически нельзя.
Использование context.Background() вместо request.Context(): при обработке HTTP запросов используйте r.Context(). Background используется для корневых задач (например, демоны), но не для per-request flow.
Слишком длинные таймауты: 60s+ используются редко; для внутренних RPC лучше 1–10s. Долгие таймауты маскируют проблемы производительности.
Передача больших данных через context: context.Value не для больших структур. Ограничение: ключи должны быть небольшими идентификаторами, значения — лёгкие.
Если вы видите в логах «context deadline exceeded» — сначала проверьте таймауты в middleware и downstream, затем измерьте p95 latency; в 70% случаев проблема в неадекватном таймауте.
Пример ошибки: код, который создаёт таймаут в middleware 2s, а downstream DB запрос иногда занимает 3–4s — в результате 30% пользовательских запросов будут отменяться. Решение: либо увеличить таймаут на middleware до 6–8s, либо оптимизировать DB запросы до p95 < 1s.
Частые вопросы
как правильно выбирать значение таймаута для HTTP запросов?
Выбор таймаута зависит от типа операции. Для внутренних RPC ориентируйтесь на 500ms–5s: 500ms для кешируемых быстрых операций, 1–3s для типичных DB-запросов, 5–10s для тяжёлых агрегаций. Для внешних интеграций устанавливайте таймаут на 25–50% ниже ожидаемого SLA стороннего API и реализуйте retry с экспоненциальным бэкоффом. Конкретика: если внешний API обещает 10s, ставьте 6s и 2 попытки с backoff 200ms->400ms.
что делать если контекст отменяется в середине транзакции базы данных?
Если контекст отменяется, нужно корректно откатить транзакцию и закрыть ресурсы. Пример: tx, err := db.BeginTx(ctx, nil); если ctx.Done() сработал, вызовите tx.Rollback() и логируйте причину ctx.Err(). На практике в 2025–2026 годах я оставлял таймаут для DB в 3s для быстрых транзакций и 10s для аналитических — если транзакция регулярно превышает эти рамки, нужно рефакторить запросы.
почему нельзя сохранять context в поле структуры?
Context хранит per-request state; если поместить его в поле структуры и переиспользовать структуру между запросами, вы получите неверное связывание ресурсов и утечки. Это приведёт к subtle багам: отмена чужого контекста или чтение устаревших значений. Вместо этого передавайте ctx в аргументах методов и при необходимости сохраняйте только конкретные value, например requestID как string.
когда использовать context.WithDeadline вместо WithTimeout?
WithDeadline полезен, когда у вас есть абсолютная временная метка. Например, если запрос должен завершиться до 2026-12-01T12:00:00Z. WithTimeout удобнее для относительных значений (5s, 500ms). Deadline применяется, когда несколько частей цепочки должны синхронизироваться по одному абсолютному времени.
Поток контекста в middleware и handlers
Дополнительно: посмотрите сопутствующие материалы по Go — руководства по Go и разборы backend на нашем ресурсе для примеров паттернов и тестовых сценариев.
Context в Go: как правильно передавать cancellation и дедлайны | KtoHto
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…