Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Зачем нужен context.Context и какую проблему он решает?
Контекст переносит по цепочке вызовов три вещи: сигнал отмены (Done()), дедлайн/таймаут и значения запроса (request-scoped values). Проблема, которую он решает: долгая операция (запрос к базе, вызов сервиса) должна уметь остановиться, когда результат уже никому не нужен — клиент ушёл, upstream истёк, сервис выключается. Без контекста горутины доделывают никому не нужную работу и тратят соединения и память. Контекст — первый параметр функции по конвенции: func Do(ctx context.Context, …).
func FetchProfile(ctx context.Context, id int64) (Profile, error) {
row := db.QueryRowContext(ctx, "SELECT ...", id) // запрос отменится вместе с ctx
...
}2. Как работает отмена: что происходит с Done()?
Context хранит канал Done() <-chan struct{}. При отмене (CancelFunc, дедлайн, отмена родителя) канал закрывается — а закрытый канал читается мгновенно любым числом читателей, поэтому один сигнал «разбуждает» всю ветку вызовов. Идиома ожидания: select { case <-ctx.Done(): return ctx.Err() case result := <-ch: … }. При отмене по дедлайну/родителю ctx.Err() вернёт context.DeadlineExceeded или context.Canceled — ошибку стоит возвращать именно её, а не свою: наверху по errors.Is поймут причину.
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled или DeadlineExceeded
case res := <-slowCh:
return res, nil
}3. Чем context.WithCancel отличается от WithTimeout и WithDeadline?
WithCancel возвращает пару (ctx, cancel) — отменяешь вручную, когда результат больше не нужен (обязательно через defer cancel()). WithTimeout(parent, d) — отмена через d от текущего момента; WithDeadline(parent, t) — отмена в конкретный момент времени (для «сделать до конца суток» по зоне). Все три наследуют отмену родителя: отмена родителя отменяет детей автоматически — дерево контекстов «разматывается» сверху вниз. Классический вопрос-ловушка: что будет, если не вызвать cancel() при WithTimeout? Утечка — таймер контекста будет держать ресурсы до срабатывания дедлайна, даже если результат давно получен.
ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // освободить таймер сразу после получения результата
4. Почему cancel() обязательно через defer? Что будет без него?
Отмена — не только про «оборвать работу», но и про освобождение внутренних ресурсов контекста: таймеров и записей в дереве родитель-дети. Пропущенный cancel() держит таймер и ссылку родителя на ребёнка до момента дедлайна — при высоком RPS это тысячи висящих таймеров и рост памяти; детектор vet даже имеет отдельную проверку lostcancel. Поэтому сигнатура отменимой операции всегда: ctx, cancel := context.WithTimeout(...); defer cancel(). Нюанс для ответа: вызов cancel() после завершения работы не «роняет» никого — это просто освобождение, идемпотентное.
5. Можно ли хранить данные в контексте (context.WithValue)? Когда это оправдано?
Технически — да, WithValue(key, val) переносит значения вниз по цепочке. Официальная позиция: только для request-scoped данных, проходящих через слои, — идентификатор запроса, токен аутентификации, трассировочный спан. Не место для «удобной передачи параметров» бизнес-логики: ключ-строку никто не отменял коллизии (ключи объявляют непользовательским типом), зависимости становятся невидимыми для компилятора и тестов, контракт функции размывается. Бизнес-зависимости — явными аргументами или структурами-сервисами. На собеседовании признание «WithValue — последний инструмент, а не первый» звучит сильнее перечисления API.
type ctxKey int const reqIDKey ctxKey = 0 ctx = context.WithValue(ctx, reqIDKey, "abc-123") reqID, _ := ctx.Value(reqIDKey).(string)
6. Контекст передаётся в горутину — что с ним не так делать?
Две ошибки противоположных направлений. Первая: не передать контекст вовсе — горутина не сможет остановиться при отмене родителя (утечка из раздела про горутины). Вторая: положить ctx в структуру или поле — контекст живёт на время запроса, а структура может пережить его; контекст должен быть аргументом каждой функции цепочки, а не состоянием. И тонкость для опытных: внутри горутины часто заводят собственный дочерний контекст (WithCancel(ctx)) — чтобы отменить её точечно, не трогая siblings.
go func() {
ctxJob, cancel := context.WithCancel(ctx) // точечная отмена
defer cancel()
process(ctxJob, job)
}()7. Как контекст связан с http.Request и базами?
В HTTP r.Context() — контекст запроса: он отменяется, когда клиент уходит или соединение закрывается. Прокидывайте его во все операции запроса: db.QueryRowContext(r.Context(), …), http.NewRequestWithContext(ctx, …) — тогда отменённый клиент мгновенно освобождает соединение с базой и не гоняет ответ в никуда. В gRPC контекст приходит первым аргументом хендлера и несёт дедлайны через сеть — цепочка дедлайнов «клиент → прокси → сервис → база» по умолчанию сквозная. За это спрашивают на системных секциях: «что произойдёт с запросом, если клиент отвалился на середине» — правильный ответ начинается с r.Context().
8. Ошибка операции истекла по контексту — что возвращать наверх?
Контекстную ошибку — ctx.Err() (Canceled / DeadlineExceeded), завёрнутую в контекст операции: fmt.Errorf("загружаю профиль: %w", ctx.Err()). Наверху по errors.Is(err, context.DeadlineExceeded) отличат «просто медленно» от «данные битые» и примут разную политику: ретрай, отдачу кэша или 503. Антипаттерн — возвращать пустую ошибку или молча глотать отмену: вызвавший не сможет ни заретраить, ни объяснить пользователю, что случилось. Ещё нюанс: после отмены частичные результаты валидны только если вы осознанно поддержали «мягкий» режим — по умолчанию отменённая операция возвращает ошибку.
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.