← Назад к вопросам

Что можно хранить в контексте?

2.0 Middle🔥 301 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что можно хранить в контексте?

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

Рекомендуемые для хранения данные

Согласно официальной документации и best practices, в контексте следует хранить:

  1. Сигналы управления выполнением:
    *   **Дедлайны (Deadlines):** Момент времени, к которому операция должна завершиться. Устанавливаются через `context.WithDeadline`.
    *   **Таймауты (Timeouts):** Продолжительность времени, отведённая на операцию. Устанавливаются через `context.WithTimeout`.
    *   **Сигналы отмены (Cancellation Signals):** Механизм для явного запроса прекращения работы. Создаётся через `context.WithCancel`.

  1. Сквозная идентификация запросов (Request-scoped data): Данные, которые относятся к конкретному запросу или цепочке вызовов и должны передаваться через все слои приложения.
    *   **ID запроса (Request ID):** Уникальный идентификатор для трассировки и логирования.
    *   **ID трассировки (Trace ID) и ID спана (Span ID):** Для распределённой трассировки (например, в Jaeger или OpenTelemetry).
    *   **Метаданные аутентификации/авторизации:** Например, ID пользователя или роли, *но не сами токены или секреты*.
    *   **Информация для логирования:** Теги, которые должны добавляться ко всем логам в рамках данного запроса.

Как правильно хранить данные: использование ключей

Данные в контексте хранятся в виде пар ключ-значение, где ключ должен быть сравнимым (comparable), а значение — потокобезопасным для одновременного доступа из нескольких горутин. Ключи должны быть уникальными и избегать коллизий. Рекомендуемый подход — использовать неэкспортируемые типы.

package main

import (
    "context"
    "fmt"
)

// Объявляем неэкспортируемый тип для ключа, чтобы избежать коллизий.
type key string

// Константа с конкретным ключом.
const requestIDKey key = "requestID"

// Функция для добавления requestID в контекст.
func withRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

// Функция для извлечения requestID из контекста.
func getRequestID(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(requestIDKey).(string)
    return id, ok
}

func main() {
    ctx := context.Background()
    ctx = withRequestID(ctx, "req-12345")

    if id, ok := getRequestID(ctx); ok {
        fmt.Printf("Request ID: %s\n", id) // Output: Request ID: req-12345
    }
}

Что НЕЛЬЗЯ хранить в контексте

  1. Неприменимые к запросу данные: Глобальные переменные, конфигурацию приложения, подключения к базам данных или кэши. Для этого используйте внедрение зависимостей (dependency injection).
  2. Критичные для бизнес-логики данные: Основные параметры функций должны передаваться явно через аргументы. Контекст — это вспомогательный механизм.
  3. Секреты и чувствительная информация: Токены, пароли, ключи API. Контекст может логироваться или сериализоваться, что приведёт к утечке.
  4. Крупные объекты или структуры: Это может привести к неявному увеличению потребления памяти и усложнению отладки.
  5. Указатели на изменяемые данные: Это нарушает потокобезопасность. Если значение может измениться, его хранение в контексте небезопасно.

Практический пример использования

func processOrder(ctx context.Context, orderID int) error {
    // 1. Проверяем, не отменён ли ещё контекст (например, пользователем).
    if err := ctx.Err(); err != nil {
        return fmt.Errorf("request cancelled before processing: %w", err)
    }

    // 2. Извлекаем сквозные данные (например, для логирования).
    if reqID, ok := getRequestID(ctx); ok {
        log.Printf("[%s] Processing order %d", reqID, orderID)
    }

    // 3. Передаём контекст вниз по цепочке вызовов (например, в HTTP-запрос или запрос к БД).
    err := chargePayment(ctx, orderID)
    if err != nil {
        return err
    }

    // 4. Операция будет автоматически прервана, если сработает дедлайн контекста.
    return updateInventory(ctx, orderID)
}

Ключевые выводы

  • Контекст — это не хранилище общего назначения. Это интерфейс для передачи сигналов управления жизненным циклом и ограниченного набора метаданных запроса.
  • Используйте контекст явно и осознанно. Он должен быть первым параметром в функции, если она выполняет операции ввода-вывода или блокирующие задачи.
  • Данные в контексте должны быть иммутабельными и потокобезопасными.
  • Всегда проверяйте ctx.Err() в долгоиграющих операциях или циклах.
  • Избегайте создания собственных типов контекста, вместо этого используйте context.WithValue с правильно объявленными ключами.

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