Context и отмена операций
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, данные аутентификации транспортного уровня. Всё, что функция реально использует для своей работы, — обычный параметр.