MBL
Go / demo / Context и отмена операций
Go средний

Context и отмена операций

concurrencycontext

context.Context — это способ передать сигнал отмены, дедлайн и request-scoped значения через цепочку вызовов, не меняя сигнатуру каждой функции под конкретный механизм отмены. Соглашение Go: контекст — всегда первый параметр, всегда называется ctx.

func fetch(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

Если ctx отменится (дедлайн истёк или вызвали cancel()), http.DefaultClient.Do вернёт ошибку почти сразу, а не будет ждать сетевого таймаута — отмена распространяется через ctx.Done() внутрь всех функций, которые её читают.

WithCancel и забытый cancel()

context.WithCancel(parent) возвращает новый контекст и функцию cancel, которую обязательно нужно вызвать, даже если операция завершилась успешно сама, без отмены.

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // без этой строки — утечка

go worker(ctx)

Причина не в самом контексте, а в дереве: пока cancel не вызван, родительский контекст держит ссылку на дочерний (для случая, если понадобится отменить и его). Если таких недо-отменённых контекстов накапливаются тысячи, это утечка памяти того же типа, что и незакрытые горутины — только источник другой: не заблокированный канал, а живой узел в дереве контекстов, который никто не освободил. WithTimeout и WithDeadline устроены так же и требуют того же defer cancel(), даже если таймаут гарантированно сработает сам — досрочный cancel() освобождает ресурсы сразу, не дожидаясь дедлайна.

WithValue — не замена параметрам

context.WithValue(ctx, key, value) кладёт значение в контекст, откуда его достают через ctx.Value(key). Соблазн — протащить туда что угодно вместо явных параметров функции. Проблема в двух вещах разом: во-первых, ctx.Value возвращает any, то есть проверка типа переезжает из компилятора в рантайм — опечатался с ключом или типом, узнаешь на проде, не на сборке. Во-вторых, сигнатура функции перестаёт быть честной: func process(ctx context.Context) не говорит читателю, что функции на самом деле нужен, например, userID — она есть, но спрятана внутри контекста.

// Плохо: userID спрятан, тип не проверяется компилятором
func process(ctx context.Context) {
    userID := ctx.Value("userID").(string) // паника, если тип не совпал
}

// Хорошо: то же самое явным параметром
func process(ctx context.Context, userID string) {
    // ...
}

Официальная рекомендация Go — WithValue только для request-scoped метаданных, которые проходят сквозь API-границы и не являются частью бизнес-логики самой функции: request ID для трейсинга, deadline, данные аутентификации транспортного уровня. Всё, что функция реально использует для своей работы, — обычный параметр.

Самопроверка 0 / 3
Понимаю, что context.Context передаётся первым аргументом и распространяет отмену вниз по дереву вызовов
Знаю, почему забытый вызов cancel() после WithCancel/WithTimeout — утечка
Могу объяснить, почему context.WithValue не замена явным параметрам функции
Как усвоено?