context в Go
Проверил, что именно отдаёт ctx.Err(). После явного cancel - context canceled. После истечения таймаута - context deadline exceeded, и сравнение с context.DeadlineExceeded даёт true. Различать эти два случая приходится в каждом сервисе. Спрашивают, зачем контекст нужен, почему cancel зовут через defer и что нельзя класть в Value.
Стержень: контекст передаёт вниз по стеку сигнал «прекращай» и дедлайн, а не параметры функции.
// Формулировки: «зачем Context?», «почему нужно звать cancel?», «что нельзя класть в Value?»
Отмена вниз по цепочке
Картинка: клиент закрыл вкладку, а твой обработчик в это время ходит в базу, оттуда в соседний сервис, а тот ещё куда-то. Отвечать уже некому, но вся цепочка продолжает жечь ресурсы. Контекст нужен, чтобы сигнал «прекращай» дошёл до самого низа.
Контексты образуют дерево: производный наследует отмену родителя. Отменили корень - каналы Done закрылись у всех потомков разом. Но отмена кооперативная: рантайм никого не убивает и убить не может. Горутина обязана сама слушать ctx.Done() в select и выходить, а та, что контекст игнорирует, спокойно доработает до конца.
// Причину узнают через ctx.Err(), и различать её полезно продуктово: на deadline exceeded обычно отвечают клиенту 504, а отмену самим клиентом обрабатывают молча, без ошибки в логах и без будильника дежурному.
- дерево контекстов
- производный наследует отмену родителя
- кооперативная отмена
- горутина сама обязана слушать Done
cancel и утечка, которую видит только vet
WithCancel, WithTimeout и WithDeadline возвращают функцию отмены. Не позвал - таймер и связь с родительским контекстом живут до самого дедлайна, даже если работа заняла миллисекунду. Посчитай: тысяча запросов в секунду при таймауте 30 секунд даёт 30 тысяч висящих таймеров вместо нуля.
Вызов cancel после нормального завершения безвреден, он просто освобождает ресурсы раньше срока. Поэтому defer cancel() ставят механически, сразу после создания, не раздумывая над каждым случаем.
// go vet проверяет это отдельно и печатает целую фразу: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak. Проверку держат в CI, потому что глазами забытый cancel не виден - код выглядит рабочим.
- defer cancel()
- освобождение ресурсов контекста в любом случае
- lostcancel
- проверка vet на потерянную функцию отмены
Что класть, а что нет
Контекст идёт первым аргументом функции, по соглашению с именем ctx. В структуры его не сохраняют: контекст живёт столько же, сколько операция, а объект обычно дольше, и связь получается бессмысленная.
В context.Value кладут только сквозные данные запроса: идентификатор трассировки, идентификатор пользователя после аутентификации. Обязательные параметры бизнес-логики туда прятать нельзя - они должны быть видны в сигнатуре, иначе вызывающий узнает о них в рантайме, по панике на приведении типа.
// Ключ делают приватным типом своего пакета, обычно type ctxKey struct{}. Строковый ключ может совпасть с чужим из другой библиотеки, а найти такую перезапись потом почти нереально.
- сквозные данные
- трассировка, идентификатор пользователя
- приватный ключ
- свой тип ключа, чтобы не пересечься с чужим кодом
Как отвечать: «Зачем нужен context.Context и почему обязательно звать cancel?»
Контекст решает две задачи: передать вниз по стеку вызовов сигнал отмены и дедлайн. Клиент отвалился или истёк таймаут запроса - и вся цепочка, включая походы в базу и в соседние сервисы, должна свернуться, а не доделывать работу впустую. Контексты образуют дерево, производные наследуют отмену родителя. Ключевая деталь: отмена кооперативная, рантайм никого не убивает, горутина обязана сама слушать ctx.Done() в select. Горутина, которая контекст игнорирует, доработает до конца, сколько её ни отменяй. Причину я различаю через ctx.Err: context canceled при явной отмене и context deadline exceeded при истечении срока, второе сравнивается с context.DeadlineExceeded. На дедлайн обычно отвечаю клиенту 504, а отмену самим клиентом обрабатываю молча, без ошибки в логах. Cancel зову всегда, через defer сразу после создания: иначе таймер и связь с родителем живут до самого дедлайна, и при тысяче запросов в секунду с таймаутом в тридцать секунд накапливается тридцать тысяч висящих таймеров. Вызов после завершения безвреден, поэтому ставлю его механически, а забытый ловит go vet проверкой lostcancel. В Value кладу только сквозные данные запроса, с приватным типом ключа, а обязательные параметры оставляю в сигнатуре.
Сильный ответ: обе задачи контекста названы, подчёркнута кооперативность отмены - это самое частое непонимание, различены две причины завершения вместе с продуктовой реакцией, а утечка без cancel посчитана числом, а не описана словом «плохо».
На чём валят
- −Не зовут cancel: таймер и связь с родителем живут до конца дедлайна.
- −Запускают горутину, которая не слушает ctx.Done(). Отмена до неё не доходит вообще.
- −Прячут обязательные параметры в context.Value вместо сигнатуры.
- −Берут строковый ключ для Value и однажды пересекаются с чужой библиотекой.
- −Сохраняют контекст в структуру, привязывая время жизни операции к времени жизни объекта.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 9, остальные разбираются в тренажёре.
- WithCancel вернул ctx и cancel. Почему cancel надо звать (обычно defer), даже если отмена не нужна?A)Иначе дочерний ctx унаследует чужой дедлайн от родителяB)Иначе утечка: ресурсы ctx и его горутина живут до отмены родителяC)Иначе следующий WithCancel вернёт тот же самый ctx повторноD)Без вызова cancel значения из ctx.Value станут недоступны всем потребителям
показать ответ и разбор
+B)Иначе утечка: ресурсы ctx и его горутина живут до отмены родителя// разбор: WithCancel заводит структуру и горутину, ждущую отмены родителя; cancel() освобождает их. Не позвать — течёт память, и горутина висит до отмены (или конца) родителя: типичная утечка, которую ловит go vet (lostcancel). Идиома — defer cancel() сразу после WithCancel/WithTimeout, даже если отменять вручную не планируешь.
- Что считается антипаттерном при использовании context.Value?A)Класть в него trace-id и дедлайн запроса на всё время его обработкиB)Передавать context первым аргументом в функции слояC)Проверять ctx.Err() после возврата из блокирующей операцииD)Прокидывать через него обязательные параметры вместо явных аргументов
показать ответ и разбор
+D)Прокидывать через него обязательные параметры вместо явных аргументов// разбор: context.Value — только для request-scoped данных, живущих поперёк слоёв (trace-id, аутентификация), и с ключами своего типа. Прокидывать им обязательные параметры бизнес-логики — антипаттерн: теряется типобезопасность и явность сигнатуры, зависимости прячутся в «мешок». Обязательное передают явными аргументами; context — для метаданных и отмены.
- Чем context.Background() отличается от context.TODO()?A)TODO отменяется автоматически при завершении функцииB)Background предназначен для main, а TODO — для горутинC)Оба пустые: TODO — пометка, что нужный контекст ещё не проведёнD)TODO несёт значения по умолчанию, Background пуст
показать ответ и разбор
+C)Оба пустые: TODO — пометка, что нужный контекст ещё не проведён// разбор: Функционально это одинаковые пустые контексты: без отмены, без дедлайна, без значений. Разница только в намерении для читателя и линтеров: Background — осознанный корень в main или на входе запроса, TODO — честная метка «сюда должен приехать контекст, но цепочка пока не протянута». Путать их не опасно, но TODO не должен доживать до прода.
- Как узнать, почему завершился контекст — по отмене или по таймауту?A)Сравнить ctx.Err() с context.Canceled и context.DeadlineExceededB)Прочитать значение из канала ctx.Done() — там лежит причинаC)Вызвать ctx.Deadline() и посмотреть, наступил ли срокD)Причина доступна только через отдельный канал, который заводят вручную
показать ответ и разбор
+A)Сравнить ctx.Err() с context.Canceled и context.DeadlineExceeded// разбор: Done лишь сигнализирует закрытием канала — значения в нём нет. Причину отдаёт Err: context.Canceled при явном cancel и context.DeadlineExceeded при истечении срока. Различать их полезно на практике: дедлайн часто транслируют клиенту как 504 и повторяют запрос, а отмену клиентом обрабатывают молча — повторять её бессмысленно.
- Каким по счёту аргументом принято передавать context.Context в функцию?A)Последним, после всех остальныхB)Первым, и по соглашению его называют ctxC)Порядок неважен, контекст ищут по типуD)Его не передают аргументом, а кладут в глобальную переменную
показать ответ и разбор
+B)Первым, и по соглашению его называют ctx// разбор: Соглашение стандартной библиотеки: контекст идёт первым параметром с именем ctx, и его не хранят в структурах. Единообразие тут не косметика — по нему видно, что функция умеет отменяться, и линтеры проверяют правило автоматически. Передавать вместо контекста nil не следует, для заглушки есть context.TODO.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.