← Все статьи

Go context: как избежать утечек и правильно отменять операции

Если вы пишете на Go больше недели, вы уже встречались с загадочным первым параметром во многих функциях — `context.Context`. К марту 2026 года этот пакет стал не просто best practice, а строгой необходимостью для построения надежных, управляемых сетевых сервисов. Однако его поверхностное использование — это билет в мир трудноотлавливаемых багов: утечек памяти, зависших горутин и непредсказуемого поведения системы под нагрузкой. Давайте забудем общие определения и сфокусируемся на самом критичном аспекте: механизме отмены (cancellation) и том, как правильно «прослушивать» сигналы о необходимости завершения работы.

Главная ментальная модель, которую нужно принять: `context.Context` — это не просто хранилище значений, а в первую очередь канал коммуникации. Родительская операция (например, HTTP-запрос пользователя или cron-задача) может послать сигнал всем своим потомкам о том, что их работа больше не нужна. Цель — не мгновенно убить процесс, а вежливо сообщить: «Пожалуйста, прекратите то, что вы делаете, освободите ресурсы и завершитесь». Игнорирование этого сигнала — основная причина «утекающих» горутин, которые продолжают висеть в памяти после того, как запрос уже обработан.

Рассмотрим типичную ошибку. Допустим, ваша горутина выполняет длительный расчет или ждет данные из канала.

go func() { // Долгая операция time.Sleep(30 * time.Second) resultCh <- calculate() }()

Что произойдет, если пользователь закроет браузер через 2 секунды? Эта горутина продолжит жить все 30 секунд, держа ссылки на объекты в памяти. Теперь исправим это с помощью контекста.

ctx:= context.Background() // Это корень. Но у него нет механизма отмены. ctxWithCancel, cancel:= context.WithCancel(ctx) defer cancel() // Важно! Вызов cancel освобождает ресурсы контекста.

go func(ctx context.Context) { select { case <-time.After(30 * time.Second): resultCh <- calculate() case <-ctx.Done(): // Слушаем сигнал отмены log.Println("Операция отменена") return // Чистое завершение } }(ctxWithCancel)

// Где-то в другом месте логики приложения: // cancel() // Вызов этой функции пошлет сигнал всем "слушателям" ctx.Done()

Ключевой момент — выбор точки `select`, где горутина может проверить `ctx.Done()`. Это должно быть место ожидания: цикл for-select, чтение из канала или блокирующий вызов (который должен поддерживать контекст).

Но здесь нас поджидает следующая ловушка: что если сама блокирующая операция (например, запрос к базе данных или внешний HTTP-вызов) не поддерживает контекст? Тогда простой проверки `ctx.Done()` перед вызовом будет недостаточно. Операция запустится и будет висеть. Решение — использование канала с таймаутом или паттерн «отмена через отдельную горутину».

Еще более практичный и часто используемый инструмент — таймауты и дедлайны. `context.WithTimeout` и `context.WithDeadline` автоматически отправляют сигнал отмены по истечении времени. Это ваш главный союзник против зависаний.

func fetchDataFromAPI(ctx context.Context) (*Data, error) { // Устанавливаем жесткий лимит на всю операцию ctxWithTimeout, cancel:= context.WithTimeout(ctx, 5*time.Second) defer cancel()

req, err:= http.NewRequestWithContext(ctxWithTimeout, "GET", url, nil) // Контекст передается в запрос if err!= nil { return nil, err }

resp, err:= http.DefaultClient.Do(req) // Этот вызов будет прерван при отмене контекста if err!= nil { return nil, err // Здесь вернется ошибка "context deadline exceeded" } defer resp.Body.Close()

//... обработка ответа }

Обратите внимание на цепочку: родительский контекст (`ctx`) передается дальше. Если он будет отменен (например, разрыв соединения клиентом), наш таймаут станет не нужен — сработает более раннее событие. Всегда передавайте контекст явно через параметры функции; никогда не храните его в глобальных переменных или структурах с долгим сроком жизни.

Теперь о распространенных антипаттернах.

Первый: создание контекста без возможности отмены (`context.Background()`) глубоко внутри бизнес-логики обработки запроса. Вы лишаете верхнеуровневую логику контроля над порожденными процессами.

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

Третий: передача `nil` вместо контекста в функцию, которая его ожидает. Если API допускает это (что является плохим дизайном), используйте `context.TODO()` вместо `nil`, чтобы явно пометить место для будущего рефакторинга.

Четвертый и самый коварный: попытка «восстановить» выполнение после получения сигнала отмены путем создания нового контекста. Если вышестоящий компонент говорит «стоп», ваша обязанность — начать процедуру остановки максимально быстро и корректно освободить ресурсы (закрыть открытые файловые дескрипторы, вернуть соединения в пул, отправить уведомление о неудаче).

Правильная работа с context.Context требует дисциплины, но она окупается сторицей. Вы получаете предсказуемое поведение сервиса под высокой нагрузкой, четкое ограничение времени выполнения операций и эффективную очистку ресурсов. Вместо хаотичного завершения горутин вы строите систему, которая чутко реагирует на изменения состояния всего приложения. Это уже не просто фича языка, а краеугольный камень архитектуры современных облачных сервисов на Go. Начните с малого: внедрите обязательную передачу контекста во все новые сетевые вызовы и всегда добавляйте select с ctx.Done() в долгоживущие горутины — ваше production-окружение скажет вам спасибо уже после первого серьезного скачка трафика

💬 Комментарии (7)
👤
igor.popov77
23.03.2026 00:07
Отличная статья! Как раз столкнулся с утечкой горутин из-за забытого контекста в HTTP-клиенте. Выручили.
👤
james.wilson85
25.03.2026 00:14
Полезный материал, но для новичков всё же сложновато. Добавьте, пожалуйста, схему жизненного цикла контекста.
👤
maria.sidorova77
26.03.2026 00:35
Статья хорошая, но хотелось бы больше примеров с реальными метриками: насколько именно растет память при утечке.
👤
rebecca.young89
28.03.2026 21:57
Интересно, а есть ли общепринятые практики по прокидыванию контекста глубоко в слои приложения (например, в репозиторий)?
👤
olga.ivanova99
30.03.2026 01:27
Работаю с Go полгода, и эта тема всегда была мутной. Теперь стало гораздо яснее, благодарю!
👤
jennifer.wilson23
31.03.2026 22:10
Спасибо за конкретику. А можно подробнее про использование context.WithTimeout в микросервисной архитектуре?
👤
james.wilson85
04.04.2026 02:48
Наконец-то понял разницу между WithCancel и WithDeadline. Автор, вы планируете сделать продолжение про context.Value?