Конкурентность: горутины, каналы и контекст

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. Как только контекст отменён — любым способом — значение фиксируется и уже не меняется.