Модуль: Concurrency · Уровень: Senior
TL;DR#
context.Context переносит сигнал отмены, дедлайн и request-scoped значения сквозь границы вызовов и горутин. Корни — Background()/TODO(); производные — WithCancel/WithTimeout/WithDeadline/WithValue. Отмена распространяется по дереву контекстов вниз; всегда вызывайте cancel() (обычно через defer), чтобы не текли ресурсы.
Теория#
Иерархия и корни#
context.Background()— пустой корень для main, init, тестов; никогда не отменяется.context.TODO()— заглушка, когда непонятно, какой контекст передать; семантически = Background, но сигнализирует «надо доделать».- Производные образуют дерево: отмена/дедлайн родителя каскадно отменяет детей.
ctx, cancel := context.WithCancel(parent)
defer cancel() // ОБЯЗАТЕЛЬНО: освобождает горутину/таймер и удаляет из дерева
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
ctx, cancel := context.WithDeadline(parent, time.Now().Add(time.Second))
defer cancel()
ctx := context.WithValue(parent, key, val) // без cancelИнтерфейс#
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{} // закрывается при отмене/дедлайне
Err() error // nil | Canceled | DeadlineExceeded
Value(key any) any
}Done() возвращает канал, который закрывается (broadcast) при отмене. Err() после закрытия даёт причину: context.Canceled или context.DeadlineExceeded.
Под капотом#
cancelCtxхранитdone(lazily созданный канал),children map[canceler]struct{},err.cancel()закрываетdone, проставляетerr, рекурсивно отменяет детей и отсоединяет себя от родителя (removeChild), чтобы не было утечки в дереве.timerCtx— этоcancelCtx+time.Timer, который вызоветcancel(DeadlineExceeded)по дедлайну.valueCtx— связный список:Value(key)идёт вверх по цепочке родителей, сравнивая ключи. O(глубины), поэтому много значений — антипаттерн.- Propagation отмены: при создании дочернего
cancelCtxрантайм проверяет, отменяем ли родитель; если да — регистрируется вchildren, иначе при необходимости запускает горутинуpropagateCancel, слушающуюparent.Done().
Распространение#
Контекст передают первым аргументом ctx context.Context во все функции, делающие I/O, RPC, долгую работу. Каждая горутина в дереве запроса получает (производный) ctx и обязана реагировать на ctx.Done().
func handle(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
return callRPC(ctx)
}
func worker(ctx context.Context, in <-chan Job) {
for {
select {
case <-ctx.Done():
return // отмена / дедлайн / завершение
case job := <-in:
process(ctx, job)
}
}
}Значения (WithValue) — best practices#
- Только request-scoped данные: trace/request ID, аутентификация, дедлайны логирования. Не для передачи опциональных параметров функции.
- Ключ — неэкспортируемый тип, чтобы избежать коллизий:
type ctxKey int
const userKey ctxKey = 0
ctx = context.WithValue(ctx, userKey, user)
u, ok := ctx.Value(userKey).(*User)Go 1.21+: WithoutCancel, AfterFunc, WithDeadlineCause, Cause#
context.WithoutCancel(ctx)— наследует значения, но игнорирует отмену родителя (для фоновых задач, переживающих запрос).context.AfterFunc(ctx, f)— вызываетfв своей горутине при отмене ctx; возвращает stop-функцию.context.WithTimeoutCause/WithDeadlineCause+context.Cause(ctx)— даёт явную причину отмены вместо общегоDeadlineExceeded.context.WithCancelCause—cancel(err)с произвольной причиной, доступной черезCause.
Подводные камни / gotchas#
- Не вызвали
cancel()→ утечка: дляWithCancelостаётся регистрация в дереве/горутина propagate; дляWithTimeout— живой таймер до дедлайна.go vetловит часть случаев (lostcancel). - Хранение Context в структуре — антипаттерн (кроме явных исключений). Передавайте как аргумент.
context.WithValueдля бизнес-параметров — типонебезопасно и непрозрачно; используйте явные аргументы.- Строковые/встроенные ключи в WithValue → коллизии между пакетами. Только приватные типы.
- Игнорирование
ctx.Done()в долгой работе делает отмену бесполезной — операция всё равно доработает до конца. - Передача
nilContext — паника/UB; используйтеTODO(). ctx.Err()без проверки Done в редких случаях гонко: правильно — читать после получения изDone()либо доверятьselect.- Дедлайн короче родительского работает; длиннее — игнорируется (родительский дедлайн всё равно отменит раньше).
Вопросы на собеседовании#
В: Зачем нужен context? О: Для сквозной передачи сигнала отмены, дедлайна и request-scoped значений через границы горутин и вызовов. Это стандартный способ скоординированно завершать дерево операций (RPC, БД, фоновые задачи) при таймауте, отмене клиентом или shutdown.
В: Разница Background и TODO?
О: Семантически идентичны (пустой неотменяемый корень). Background — для входных точек (main, тесты, верх стека); TODO — маркер «контекст ещё не проброшен/неясен», помогает статическому анализу и ревью.
В: Что возвращает Done() и когда срабатывает?
О: Канал <-chan struct{}, который закрывается при отмене или дедлайне (broadcast всем слушателям). После закрытия Err() возвращает Canceled или DeadlineExceeded.
В: Почему обязательно вызывать cancel?
О: cancel закрывает done, отменяет детей и отвязывает контекст от родителя; для timer-контекстов останавливает таймер. Без него — утечка горутины/таймера и рост дерева контекстов. go vet предупреждает о потерянном cancel.
В: Как распространяется отмена по дереву?
О: При отмене узла закрывается его done, затем рекурсивно отменяются все зарегистрированные дети. Дочерний cancelCtx либо регистрируется напрямую в родителе, либо (если родитель не «родной» cancelCtx) обслуживается горутиной propagateCancel, слушающей parent.Done().
В: Как правильно класть значения?
О: Только request-scoped данные, с приватным типом ключа, чтобы исключить коллизии. Не использовать для передачи обычных аргументов. Доступ — type assertion с проверкой ok. Помнить про O(глубины) при чтении.
В: Как сделать фоновую задачу, переживающую запрос?
О: context.WithoutCancel(ctx) сохранит значения (trace id и т.п.), но отвяжет от отмены родителя; либо новый Background() с переносом нужных значений. Для запуска кода по отмене — context.AfterFunc.
В: Чем отличается Canceled от DeadlineExceeded и что такое Cause?
О: Canceled — явный вызов cancel; DeadlineExceeded — истёк дедлайн. С 1.21 WithCancelCause/WithTimeoutCause позволяют задать произвольную причину, читаемую context.Cause(ctx), при этом Err() остаётся стандартным.
На что копают на senior+#
- Устройство
cancelCtx/timerCtx/valueCtx, lazy-созданиеdone,removeChildи почему это предотвращает утечку дерева. propagateCancel: когда запускается отдельная горутина-наблюдатель и её стоимость.- Семантика
Cause/WithoutCancel/AfterFunc(1.21) и их применение. - Корректная обработка отмены в библиотечном коде (не «глотать» ctx.Err, прокидывать дедлайн в нижележащие вызовы).
- Взаимодействие context с
errgroup,http.Servershutdown, database/sql (ctx в каждом запросе). - Почему хранение ctx в структуре ломает контракт и когда это всё же допустимо (например, в
http.Request).