Что такое `context propagation` и как этот механизм реализован в Go?

Ответ

Что такое Context Propagation?

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

В Go этот механизм реализован через стандартный пакет context и интерфейс context.Context.

Зачем он нужен?

Контекст решает три основные задачи:

  1. Отмена операций (Cancellation): Позволяет грациозно остановить цепочку горутин, если родительская операция была отменена (например, пользователь закрыл вкладку браузера).
  2. Контроль времени выполнения (Deadlines/Timeouts): Гарантирует, что операция не будет выполняться дольше заданного времени.
  3. Передача данных в рамках запроса (Request-scoped data): Позволяет передавать метаданные (ID запроса, токен аутентификации) вниз по стеку вызовов, не загрязняя сигнатуры функций.

Как это работает в Go?

  • context.Background(): Пустой корневой контекст, который используется как отправная точка для всех контекстов.
  • context.WithCancel(parent): Создает дочерний контекст, который можно отменить, вызвав возвращаемую функцию cancel().
  • context.WithTimeout(parent, duration): Создает дочерний контекст, который автоматически отменяется по истечении таймаута.
  • context.WithValue(parent, key, value): Добавляет в контекст данные типа ключ-значение.

Пример:

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

// Передаем контекст в долгую операцию
result, err := longOperation(ctx)
if err != nil {
    // Ошибка может быть вызвана таймаутом
    if errors.Is(err, context.DeadlineExceeded) {
        fmt.Println("Операция отменена по таймауту")
    }
}

func longOperation(ctx context.Context) (string, error) {
    select {
    case <-time.After(3 * time.Second): // Имитация долгой работы
        return "Результат", nil
    case <-ctx.Done(): // Канал Done() закрывается при отмене или таймауте
        return "", ctx.Err() // Возвращаем ошибку контекста
    }
}

Ключевые принципы:

  • Иммутабельность: Контекст неизменяем. Функции WithCancel, WithTimeout и WithValue возвращают новый, дочерний контекст.
  • Явная передача: Контекст всегда передается как первый аргумент функции: func DoSomething(ctx context.Context, ...).
  • Не для бизнес-логики: Не используйте context.WithValue для передачи обязательных параметров. Он предназначен для сквозных, request-scoped данных.

Ответ 18+ 🔞

А, контекст! Ну это ж классика, блядь, просто пиздец! Сейчас объясню на пальцах, а то некоторые, как мартышлюшки, в коде его суют куда попало, а потом охуевают, почему всё падает.

Смотри, представь себе: ты в баре заказываешь пиво и чипсы. Это твой запрос. А контекст — это, сука, такая записка от бармена официантке: «Этому лысому мудаку за столиком №3 — пиво «Балтика», чипсы со вкусом краба, и если он начнёт скандалить про политику — сразу вышвыривай нахуй, отмена».

Вот эта записка и есть context.Context. Она бегает по всей твоей программе, от функции к функции, и несёт три священных знания:

  1. Когда всё кончилось (Cancellation). Официантка (твоя горутина) слушает, не крикнул ли бармен «ВСЁ, ЗАКРЫВАЕМСЯ!». Крикнул — она сразу бросает поднос и идёт домой. Не ждёт, пока пиво станет тёплым. Это ctx.Done().
  2. Сколько времени ждать (Deadline/Timeout). В записке приписка: «Если через 5 минут не допивает — выгоняй». Это context.WithTimeout. Время вышло — официантка выкидывает твоё недопитое пиво в раковину и говорит «Извини, чувак, таймаут».
  3. Что именно нести (Request-scoped data). В записке ещё написано: «Он у нас VIP, дай ему соломинку золотую». Это context.WithValue. Любая функция в цепочке (официантка, повар, мойщик стаканов) может глянуть в записку и сказать: «А, так это тот самый лысый VIP! Надо золотую соломинку».

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

Правило первое, железобетонное: Контекст — он как девственность. Его нельзя изменить, блядь! Ты можешь только взять существующий (parent) и породить от него нового, улучшенного ребёнка с дополнительными условиями.

// Это твой папа-контекст, пустой и чистый
ctx := context.Background()

// А это его сынок, но с таймером на 2 секунды
ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel() // И не забудь потом пристрелить сыночка, когда он не нужен!

Правило второе, как удар вилкой в глаз: Контекст всегда, блядь, ВСЕГДА идёт ПЕРВЫМ аргументом в функцию. Это как пароль «Мир-дружба-жвачка». Без него — не пущат.

// Правильно, как у взрослых:
func doSomeMagic(ctx context.Context, userId int) error {...}

// А так пишут полупидоры, которых потом увольняют:
func doSomeMagic(userId int, ctx context.Context) error {...} // ПИЗДЕЦ КОД

Правило третье, про данные: Не суй в контекст всё подряд, ёпта! Это не глобальный мешок для говна. Только для того, что действительно относится ко всему запросу: ID запроса, токен, метрики. Нельзя туда пихать database connection или, блядь, структуру пользователя. Это пиздопроебищно.

// Допустимо, если очень надо:
type ctxKey string
var requestIDKey ctxKey = "request_id"
ctx := context.WithValue(parentCtx, requestIDKey, "abc-123")

// А вот это — манда с ушами, не делай так:
ctx := context.WithValue(parentCtx, "entire_database_pool", dbPool) // ИДИ НАХУЙ

Вот смотри, живой пример, почему это охуенно:

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

    // Кидаем контекст в чёрную дыру
    result, err := callBlackHole(ctx)
    if err != nil {
        // Ошибка может быть потому, что таймаут сработал!
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("Бля, чёрная дыра думала дольше 2 секунд! Отмена!")
        }
    }
}

func callBlackHole(ctx context.Context) (string, error) {
    // Она думает ровно 3 секунды
    select {
    case <-time.After(3 * time.Second):
        return "Ответ от сингулярности", nil
    case <-ctx.Done(): // А вот тут слушаем контекст!
        // Если сработал таймаут или отмена, мы сваливаем отсюда
        return "", ctx.Err() // Возвращаем честную причину: "дедлайн проёбан"
    }
}

И что мы получим? Через 2 секунды программа скажет «Бля, таймаут!» и не будет тупо ждать третьей секунды от чёрной дыры. Всё грациозно отменится, ресурсы освободятся. Красота, блядь!

Короче, запомни: контекст — это не просто какая-то хуйня, это система управления твоим асинхронным бардаком. Используй с умом, не выёбывайся с WithValue, и будет тебе счастье. А если забудешь defer cancel() — готовься к утечкам горутин, они тебя сожрут, как сабаки сука.