Какие стандартные функции для создания контекстов в Go вы знаете? Расскажите о `context.WithValue` и его правильном применении.

Ответ

В стандартной библиотеке Go есть несколько ключевых функций для работы с контекстами:

  • context.Background(): Создает пустой корневой контекст. Обычно используется в main(), инициализаторах и тестах как контекст верхнего уровня.
  • context.TODO(): Так же, как и Background(), создает пустой контекст. Используется как временная заглушка, когда неясно, какой контекст использовать, или когда функция еще не адаптирована для работы с контекстом.
  • context.WithCancel(parent): Создает дочерний контекст и возвращает функцию cancel. При вызове cancel() этот контекст и все его дочерние контексты отменяются.
  • context.WithDeadline(parent, time): Создает контекст, который отменяется по наступлению определенного времени (time).
  • context.WithTimeout(parent, duration): Удобная обертка над WithDeadline, которая отменяет контекст по истечении duration.

context.WithValue

context.WithValue используется для передачи request-scoped данных, то есть данных, относящихся к конкретному запросу, например, ID запроса, токен аутентификации или информация для трассировки.

Правила и лучшие практики:

  1. Не для бизнес-логики: Никогда не передавайте через контекст обязательные параметры функции (например, ID пользователя для выборки из БД). Это делает зависимости неявными и усложняет тестирование и понимание кода. Параметры должны передаваться в функцию явно.
  2. Используйте кастомные типы ключей: Чтобы избежать коллизий ключей между разными пакетами, в качестве ключа следует использовать неэкспортируемый кастомный тип.

Пример правильного использования (передача ID запроса для логирования):

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

const requestIDKey key = 0

func main() {
    // Предположим, это middleware, который добавляет ID к каждому запросу.
    requestID := "abc-123-xyz-789"
    ctx := context.WithValue(context.Background(), requestIDKey, requestID)

    // Передаем контекст дальше по цепочке вызовов.
    processRequest(ctx)
}

func processRequest(ctx context.Context) {
    // Извлекаем ID запроса для логирования.
    if reqID, ok := ctx.Value(requestIDKey).(string); ok {
        log.Printf("[%s] Обработка запроса...", reqID)
    } else {
        log.Printf("Обработка запроса... (ID не найден)")
    }
}