Что такое `context.Context` в Go и для чего он используется?

Ответ

context.Context в Go — это стандартный механизм для передачи дедлайнов, сигналов отмены и других данных (request-scoped values) между горутинами. Он является неотъемлемой частью написания надежного и управляемого конкурентного кода, особенно в сетевых сервисах и распределенных системах.

Основные сценарии использования:

  1. Отмена (Cancellation): Если пользователь отменил запрос (например, закрыл вкладку браузера), context позволяет каскадно передать сигнал отмены всем горутинам, которые обрабатывали этот запрос (например, запросы к БД, другим сервисам), чтобы они прекратили работу и освободили ресурсы.

    • Создается с помощью context.WithCancel().
    • Проверяется через канал <-ctx.Done().
  2. Таймауты и Дедлайны (Timeouts and Deadlines): Можно установить максимальное время на выполнение операции. Если время истекло, контекст будет отменен. Это критически важно для предотвращения "зависания" горутин в ожидании ответа от медленного внешнего сервиса.

    • Создается с помощью context.WithTimeout() или context.WithDeadline().
  3. Передача данных (Value Propagation): Позволяет передавать данные вниз по стеку вызовов, например, ID запроса, информацию для трассировки или данные о пользователе.

    • Создается с помощью context.WithValue().
    • Важно: Считается плохой практикой использовать WithValue для передачи обязательных параметров функции. Параметры должны передаваться явно. Контекст предназначен только для сквозных, request-scoped данных.

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

package main

import (
    "context"
    "fmt"
    "time"
)

func longOperation(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("Операция успешно завершена")
    case <-ctx.Done(): // Этот канал закроется при отмене контекста
        fmt.Println("Операция прервана по таймауту:", ctx.Err())
    }
}

func main() {
    // Создаем контекст с таймаутом в 2 секунды
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel() // Важно вызывать cancel, чтобы освободить ресурсы

    longOperation(ctx) // Операция будет прервана через 2 секунды
}

Правила использования Context:

  • Передавайте Context как первый аргумент функции, с именем ctx.
  • Никогда не храните Context внутри структуры. Передавайте его явно в каждый вызов.
  • Цепочка вызовов должна пробрасывать контекст дальше.
  • Никогда не передавайте nil Context. Если не уверены, какой использовать, используйте context.Background().

Ответ 18+ 🔞

Слушай, а вот этот твой context.Context в Go — это ж, блядь, такая хитрая жопа, которая по всему твоему коду бегает и всем передаёт, что пора закругляться, потому что пользователь уже в ротберунчик ушёл и вкладку закрыл. Представь себе: ты пишешь какой-нибудь микросервис, а там дохуя горутин на каждый запрос — одна к базе лезет, другая в другой сервис стучится, третья там хуй знает что делает. И вот если клиент свалил, а они все ещё работают — это пиздец, ресурсы жрут, память занимают. Вот контекст как раз и кричит им: «Э, сука, расслабьтесь, уже никому не надо!».

Зачем он, этот ёперный театр, вообще нужен?

  1. Отмена (Cancellation): Это когда ты говоришь всем своим подчинённым горутинам: «Всё, ребята, пиздуй отсюда, работа отменяется!». Например, пользователь нажал «стоп» или соединение разорвалось. Создаёшь контекст с помощью context.WithCancel(), получаешь функцию cancel(). Вызвал её — и всем, кто этот контекст слушает, прилетает сигнал в <-ctx.Done().
  2. Таймауты и дедлайны (Timeouts and Deadlines): А это когда ты говоришь: «У тебя есть ровно две секунды, чтобы сделать своё дело, иначе я тебя сам выключу, пидарас шерстяной». Не хочешь, чтобы твой сервис вечно ждал ответа от медленной как черепаха базы данных. context.WithTimeout() или context.WithDeadline() — твои лучшие друзья.
  3. Передача данных (Value Propagation): Ну, типа, передать какие-то служебные штуки вглубь вызовов. ID запроса для логов, данные пользователя для авторизации. context.WithValue() для этого. Но, блядь, смотри — это не панацея! Не начинай туда пихать все параметры функций, а то получится мудя нечитаемая. Только для действительно сквозных, request-scoped данных. Обязательные аргументы — передавай явно, нормальным способом.

Вот смотри, живой пример с таймаутом, чтобы было понятно:

package main

import (
    "context"
    "fmt"
    "time"
)

func longOperation(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("Операция успешно завершена")
    case <-ctx.Done(): // Сюда сигнал прилетит, если контекст отменят
        fmt.Println("Операция прервана по таймауту:", ctx.Err())
    }
}

func main() {
    // Дали операции 2 секунды. Не успела — в пизду.
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel() // Это ОБЯЗАТЕЛЬНО вызывать, даже если таймаут сам сработает. Ресурсы освободить, блядь!

    longOperation(ctx) // Она попытается работать 3 секунды, но мы её вырубим через 2.
}

А теперь, блядь, главные правила, чтобы не выстрелить себе в ногу:

  • Контекст всегда передавай первым аргументом в функцию. И называй его ctx. Это как священная корова, все так делают.
  • Никогда, слышишь, НИКОГДА не засовывай контекст в поля какой-нибудь структуры. Он должен пробрасываться явно, по цепочке вызовов, как горячая картошка.
  • Если функция принимает контекст — ты обязан его пробросить дальше, во все вызовы, которые его тоже принимают.
  • Если тебе не с чем работать, используй context.Background(). nil контекст передавать — это высший пилотаж идиотизма, на такое даже смотреть больно.

Вот и весь сказ, блядь. Освоишь контекст — будешь писать код, который не зависает нахуй и адекватно реагирует на внешний мир. А не будет как та Муму, которую в озеро бросили.