context.Context: отмена и таймаут по цепочке вызовов
Содержание курса
WithCancel и WithTimeout: создание дочернего контекста и обязательный defer cancel
От корневого context.Background() вы не можете послать сигнал отмены — он неотменяем по определению. Чтобы получить контекст, который можно остановить, нужен дочерний:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Две вещи происходят одновременно: появляется дочерний контекст ctx и функция cancel, которая его останавливает. defer cancel() должна быть зарегистрирована немедленно — следующей строкой после вызова. Это не формальность.
Что делает cancel? Она закрывает ctx.Done(), удаляет связь дочернего контекста с родителем и отменяет всех потомков ctx. Без вызова cancel эта связь остаётся живой до тех пор, пока родительский контекст сам не завершится — возможно, никогда. Повторный вызов cancel() безопасен: он просто ничего не делает.
WithTimeout добавляет автоматический таймер:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
Через 2 секунды контекст отменится сам. Но defer cancel() всё равно нужен — и вот почему: если работа завершится за 500 мс, внутренний таймер продолжит жить ещё 1.5 секунды, удерживая ресурсы. cancel() гасит таймер досрочно.
Как различить причину отмены? Через ctx.Err():
- Явный вызов
cancel()→context.Canceled - Истёк таймаут →
context.DeadlineExceeded - Контекст ещё активен →
nil
Пока функция работает нормально, ctx.Err() возвращает nil. Как только контекст отменён — любым способом — значение фиксируется и уже не меняется.
