Для чего используется `context.WithValue` и какие существуют лучшие практики его применения?

Ответ

Функция context.WithValue используется для передачи метаданных, специфичных для запроса, по цепочке вызовов, особенно между горутинами. Это позволяет избежать "пробрасывания" таких данных через аргументы каждой функции.

Типичные примеры данных, передаваемых через контекст:

  • ID запроса (request ID) для трассировки.
  • Данные аутентификации пользователя.
  • Информация для распределенной трассировки (tracing spans).

Лучшие практики и важные правила:

  1. Не использовать для обязательных параметров. Контекст не должен использоваться для передачи обязательных аргументов функции. Такие параметры должны быть явными аргументами функции. Это делает API вашей функции понятным и самодостаточным.

    • Плохо: func DoSomething(ctx context.Context) (где ID пользователя извлекается из ctx).
    • Хорошо: func DoSomething(ctx context.Context, userID int).
  2. Использовать кастомные типы для ключей. Никогда не используйте встроенные типы (например, string) в качестве ключей для context.WithValue. Это может привести к коллизиям имен между разными пакетами. Создайте свой собственный неэкспортируемый тип.

    // Создаем неэкспортируемый тип для ключа
    type key int
    
    // Создаем экземпляр ключа
    const userIDKey key = 0
    
    // Запись значения
    ctx = context.WithValue(ctx, userIDKey, 123)
    
    // Чтение значения
    userID, ok := ctx.Value(userIDKey).(int)
  3. Контекст неизменяем (Immutable). context.WithValue возвращает новый объект Context с добавленным значением, а не изменяет существующий. Всегда присваивайте результат обратно переменной контекста: ctx = context.WithValue(ctx, key, value).

  4. Храните только транзитные данные. Контекст предназначен для данных, жизненный цикл которых совпадает с жизненным циклом запроса. Не храните в нем ссылки на объекты с долгим временем жизни (например, пулы соединений с БД или логгеры).