логотип PurpleSchool
логотип PurpleSchool

Пакет Context в Go

Автор

Александр Гольцман

Пакет Context в Go

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

В этой статье я расскажу, как работает пакет context в Go, объясню его ключевые концепции — Context, WithCancel, WithTimeout, WithDeadline и WithValue, а также покажу примеры использования в реальных задачах.

Зачем нужен пакет Context

При работе с сетевыми запросами, базами данных или горутинами важно управлять временем выполнения операций. Например, если запрос к внешнему API длится слишком долго, его нужно отменить, чтобы освободить ресурсы. Здесь на помощь приходит пакет context.

Основные задачи context:

  • Контроль времени выполнения: установка тайм-аутов и дедлайнов.
  • Отмена операций: возможность прекратить работу сразу нескольких горутин.
  • Передача метаданных: добавление контекста (например, идентификаторов пользователей) в запросы.

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

Основные типы Context в Go

Go предоставляет четыре ключевые функции для создания контекста:

  • context.Background() — создаёт пустой контекст, обычно используется как корневой.
  • context.TODO() — применяется, когда контекст пока неизвестен или в процессе разработки.
  • context.WithCancel() — создаёт контекст, который можно отменить вручную.
  • context.WithTimeout() — создаёт контекст с автоматической отменой по тайм-ауту.
  • context.WithDeadline() — создаёт контекст с автоматической отменой в заданное время.
  • context.WithValue() — создаёт контекст с дополнительными данными.

Все производные контексты (WithCancel, WithTimeout, WithDeadline) возвращают два значения: новый контекст и функцию cancel(). Важно вызывать эту функцию, чтобы освободить ресурсы, особенно при работе с сетевыми операциями или базами данных.

Создание и отмена контекста с WithCancel

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

Смотрите пример, где мы запускаем горутину и управляем её остановкой с помощью контекста:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Горутина остановлена")
                return
            default:
                fmt.Println("Работаю...")
                time.Sleep(500 * time.Millisecond)
            }
        }
    }(ctx)

    time.Sleep(2 * time.Second)
    fmt.Println("Отмена контекста")
    cancel()

    time.Sleep(1 * time.Second) // Даём время горутине завершить работу
}

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

Управление временем с WithTimeout и WithDeadline

Иногда операции должны прерываться автоматически по истечении заданного времени. Для этого используются контексты с тайм-аутом или дедлайном:

  • WithTimeout — задаёт тайм-аут относительно текущего времени.
  • WithDeadline — устанавливает точное время окончания операции.

Смотрите пример, где я ограничиваю выполнение операции двумя секундами:

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    select {
    case <-time.After(3 * time.Second):
        fmt.Println("Операция выполнена")
    case <-ctx.Done():
        fmt.Println("Контекст завершён:", ctx.Err())
    }
}

Здесь операция прервётся через 2 секунды, даже если основная задача выполняется дольше. Функция ctx.Done() сигнализирует о завершении контекста, а метод ctx.Err() показывает причину — чаще всего context.DeadlineExceeded.

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

Контекст может использоваться для передачи метаданных между функциями — например, идентификатора запроса, имени пользователя или параметров аутентификации. Для этого служит функция WithValue.

Важно понимать: контекст не предназначен для передачи больших объектов или состояний приложения. Используйте его только для данных, связанных с выполнением запроса.

Смотрите пример:

func main() {
    ctx := context.WithValue(context.Background(), "requestID", "12345")
    handleRequest(ctx)
}

func handleRequest(ctx context.Context) {
    requestID := ctx.Value("requestID")
    fmt.Println("Обрабатываю запрос с ID:", requestID)
}

Здесь я добавил в контекст идентификатор запроса, а затем извлёк его в обработчике. Использование WithValue особенно удобно в микросервисах и HTTP-серверах для передачи данных между промежуточными слоями (middleware).

Контекст в реальных задачах

Работа с HTTP-сервером

Контекст широко применяется в веб-серверах для обработки запросов. Смотрите пример сервера, который автоматически прерывает долгие запросы:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(2 * time.Second):
        fmt.Fprintln(w, "Операция завершена")
    case <-ctx.Done():
        fmt.Fprintln(w, "Запрос отменён:", ctx.Err())
    }
})

srv := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 5 * time.Second,
}
srv.ListenAndServe()

Если клиент оборвёт соединение или истечёт тайм-аут, обработчик получит сигнал из ctx.Done(), и запрос завершится корректно.

Работа с базой данных

С помощью context можно прерывать запросы к базе данных, если они выполняются слишком долго:

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
    log.Println("Ошибка запроса:", err)
    return
}
defer rows.Close()

for rows.Next() {
    var user string
    rows.Scan(&user)
    fmt.Println("Пользователь:", user)
}

Если запрос превысит установленный тайм-аут, контекст автоматически его остановит, предотвращая блокировку базы данных.

Ошибки при работе с Context

  • Использование context.TODO() в продакшене: этот контекст подходит только для черновых версий.
  • Избыточное использование WithValue для передачи данных: контекст не предназначен для передачи больших объектов.
  • Отсутствие вызова cancel() после создания контекста: обязательно освобождайте ресурсы.
  • Создание контекста внутри зависимых функций: контекст должен передаваться сверху вниз по цепочке вызовов, а не создаваться заново.

Заключение

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

В этой статье я показал:

  • Как создавать контексты и управлять их жизненным циклом с помощью WithCancel, WithTimeout, WithDeadline и WithValue.
  • Как использовать контекст в реальных задачах: при работе с HTTP-серверами и базами данных.
  • Какие ошибки можно допустить и как их избежать.

Смотрите, context особенно полезен в микросервисной архитектуре, при работе с базами данных и внешними API. Он помогает делать программы более надёжными, экономить ресурсы и предотвращать утечки. Используйте контекст всякий раз, когда работаете с долгими операциями или параллельными задачами — так вы получите более управляемый и предсказуемый код.

Стрелочка влевоПакеты crypto в GoМаршрутизатор chi в GolangСтрелочка вправо

Все гайды по Golang

Работа с YAML в GolangПреобразование типов в GolangКонвертация структур в JSON в GolangStrconv в GolangИспользование пакета SQLx для работы с базами данных в GolangРазбираемся с SQL в GolangРазделение строк с помощью функции split в GolangSort в GoПоиск и замена строк в Go - GolangИспользование пакета reflect в GolangРабота с PostgreSQL в GoPointers в GolangПарсинг в GoРабота со списками (list) в GolangПреобразование int в string в GolangРабота с числами с плавающей точкой в GolangРабота с полями в GolangИспользование enum в GolangОбработка JSON в GoЧтение и запись CSV-файлов в GolangРабота с cookie в GolangРегистры в GoКэширование данных в GolangПреобразование byte в string в GolangByte в GoИспользование bufio для работы с потоками данных в GolangДобавление данных и элементов (add) в Go
Логирование в Golang. Zap, Logrus, Loki, GrafanaРабота с Docker-контейнерами в GoИспользование pprof в GolangМеханизмы синхронизации в GolangРабота с пакетом S3 в GolangМониторинг Golang приложений с помощью PrometheusОптимизация проектов на GoПаттерны проектирования в GolangМиграции базы данных в GolangОркестрация контейнеров Go с Kubernetes + DockerGjGo Playground и компилятор GolangИспользование go mod init для создания модулей GolangРабота с переменными окружения (env) в GolangКоманда go build в GolangАвтоматизация Golang проектов — CI/CD с GitLab CI и JenkinsОтладка кода в GolangЧтение и использование конфигурации в приложениях на GolangКомпиляция в GolangКак развернуть Go-приложение на облаке AWSАутентификация в Golang
Сетевые протоколы в GoПеременные в GolangЗначения в GolangДженерик %T и его применение в GolangТипы данных в GolangИспользование tls в GolangИспользование tag в структурах GolangSwitch в GoСтроки в GolangРабота с потоками (stream) в GolangSelect в GoРуны в GoРабота с пакетом params в GolangКонвертация строк в числа в GolangNull, Nil, None, 0 в GoНаименования переменных, функций и структур в GoInt в GolangУстановка GolangЧтение и установка HTTP заголовков в GolangMethods в GolangGoLand — IDE для разработки на Golang от JetBrainsОбработка «not found» в GolangFloat в GolangФлаги командной строки в Go (Golang)Запуск внешних команд в GolangОбработка ошибок в GoИспользование defer в GolangЗначения default в GolangГенерация кода в GoФорматирование кода в GolangЧистая архитектура в GolangКаналы (channels) в GolangПолучение body из HTTP запроса в Golang
Открыть базу знаний