Найти в Дзене
all about it

Как правильно использовать context в Go: от основ до продвинутых практик

Пакет context — один из самых важных инструментов в Go для управления отменой операций, таймаутами и передачей данных между горутинами. Однако многие разработчики используют его неправильно, что приводит к утечкам памяти или неочевидным багам. В этой статье разберём: context решает три ключевые задачи: func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel() // Освобождаем ресурсы
result, err := someLongOperation(ctx)
if err != nil {
http.Error(w, "Operation timed out", http.StatusGatewayTimeout)
return
}
fmt.Fprintf(w, "Result: %v", result)
} Что происходит? МетодОписаниеWithCancelСоздаёт контекст с возможностью отмены (cancel()).WithTimeoutАвтоматическая отмена через заданное время.WithDeadlineОтмена в конкретный момент времени (например, 2025-01-01 00:00:00).WithValueПередача данных (например, userID или requestID). // Плохо: контекст — не место для сложных структур!
Оглавление

Введение

Пакет context — один из самых важных инструментов в Go для управления отменой операций, таймаутами и передачей данных между горутинами. Однако многие разработчики используют его неправильно, что приводит к утечкам памяти или неочевидным багам.

В этой статье разберём:

  1. Базовые сценарии использования context.
  2. Распространённые ошибки и как их избежать.
  3. Практические примеры из реальных проектов.

1. Зачем нужен context?

context решает три ключевые задачи:

  1. Отмена операций (например, если пользователь отменил HTTP-запрос).
  2. Таймауты (чтобы не ждать ответа вечно).
  3. Передача данных (например, request ID для логирования).

Пример: HTTP-сервер с таймаутом

func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel() // Освобождаем ресурсы

result, err := someLongOperation(ctx)
if err != nil {
http.Error(w, "Operation timed out", http.StatusGatewayTimeout)
return
}
fmt.Fprintf(w, "Result: %v", result)
}

Что происходит?

  • Если someLongOperation не завершится за 2 секунды, ctx автоматически отменится.
  • defer cancel() гарантирует, что контекст освободит ресурсы, даже если операция завершится раньше.

2. Основные функции context

МетодОписаниеWithCancelСоздаёт контекст с возможностью отмены (cancel()).WithTimeoutАвтоматическая отмена через заданное время.WithDeadlineОтмена в конкретный момент времени (например, 2025-01-01 00:00:00).WithValueПередача данных (например, userID или requestID).

3. Опасные антипаттерны

❌ Хранение больших объектов в context

// Плохо: контекст — не место для сложных структур!
ctx := context.WithValue(context.Background(), "user", User{Name: "Alice"})

Почему плохо?

  • Усложняет отладку.
  • Нет гарантий типа (interface{}).

Решение:

  • Передавайте только скалярные значения (ID, флаги).
  • Используйте специальные ключи (не строки!):
type key string
const userKey key = "user"
ctx := context.WithValue(ctx, userKey, User{Name: "Alice"})

❌ Игнорирование отмены контекста

// Плохо: горутина "утечёт", если контекст отменится.
go func() {
for {
select {
case <-time.After(1 * time.Second):
fmt.Println("Working...")
}
}
}()

Решение:
Всегда проверяйте ctx.Done():

go func(ctx context.Context) {
for {
select {
case <-ctx.Done(): // Выход при отмене
return
case <-time.After(1 * time.Second):
fmt.Println("Working...")
}
}
}(ctx)

4. Практика: Graceful Shutdown сервера

Graceful Shutdown (Корректное завершение работы) — это контролируемый процесс завершения работы приложения или сервиса, при котором:

  • Завершаются все текущие операции
  • Сохраняются данные
  • Закрываются сетевые соединения
  • Освобождаются ресурсы (память, файловые дескрипторы)
  • Отправляются уведомления клиентам/зависимым сервисам

В отличие от принудительного завершения, graceful shutdown предотвращает потерю данных, зависание соединений и утечки ресурсов. Это особенно важно для микросервисов, баз данных и высоконагруженных систем.

Пример, как корректно завершать работу сервера при получении SIGTERM:

func main() {
server := &http.Server{Addr: ":8080"}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx) // Завершаем работу с таймаутом
}()

if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}

Как это работает?

  1. signal.NotifyContext ловит Ctrl+C или SIGTERM.
  2. При получении сигнала вызывается server.Shutdown().
  3. Если сервер не завершится за 5 секунд — shutdownCtx отменится.

5. Выводы

  • ✅ Используйте context для отмены и таймаутов.
  • ❌ Не храните в нём сложные данные.
  • 🔧 Всегда вызывайте defer cancel().
  • 🛡 Проверяйте ctx.Done() в долгих операциях.

Вопрос читателям:

Какие ещё подводные камни context вы встречали в своих проектах? Делитесь в комментариях!