Перейти к содержимому

Контекст: отмена, дедлайны и значения запроса

context.Context — это интерфейс из стандартной библиотеки (пакет context, появился в Go 1.7, до этого жил как golang.org/x/net/context), который решает ровно одну инженерную задачу: как оповестить дерево горутин, обслуживающих один запрос, что работу пора прекращать. В Go нет способа «убить» горутину извне: goroutine.Kill() не существует, и остановиться горутина может только сама, добровольно, увидев сигнал. Контекст — это стандартизованный канал такого сигнала плюс договорённость передавать его первым аргументом во все блокирующие функции: func Do(ctx context.Context, ...).

Модель, которую надо держать в голове, — дерево (точнее, односвязный список вверх). Контекст иммутабелен: WithCancel, WithTimeout, WithDeadline, WithValue не меняют родителя, а возвращают новый узел, который хранит указатель на родителя. Отмена распространяется строго вниз: отменили узел — закрылись Done()-каналы всех его потомков; отменили потомка — родителю всё равно. Поиск значения (Value) идёт строго вверх по цепочке до первого совпадения ключа. Отсюда две ключевые характеристики: отмена — O(количество потомков) при широковещании и мгновенна, а Value — O(глубины цепочки) линейный проход, никакой хеш-таблицы внутри нет.

Интерфейс минимален — четыре метода: Deadline() (time.Time, bool), Done() <-chan struct{}, Err() error, Value(key any) any. Всё остальное в пакете — это функции-конструкторы, возвращающие приватные реализации: backgroundCtx/todoCtx (обе поверх пустого emptyCtx), *cancelCtx, *timerCtx, *valueCtx, withoutCancelCtx. Механизм отмены — закрытие канала: Done() возвращает <-chan struct{}, который никто никогда не пишет, его только закрывают, а закрытие канала — это широковещательный сигнал: все читатели, сколько бы их ни было, мгновенно разблокируются. Err() после отмены возвращает context.Canceled либо context.DeadlineExceeded, до отмены — nil.

Практическая дисциплина сводится к нескольким правилам. Контекст передаётся аргументом, а не кладётся в структуру (исключение — структуры, которые сами моделируют запрос). cancel, возвращённый любым With*-конструктором, обязателен к вызову, обычно через defer cancel(), иначе узел остаётся висеть в children родителя и таймер живёт до срабатывания — это классическая утечка. Через WithValue кладут только то, что относится к запросу и пересекает границы API (trace ID, request ID, данные аутентификации), а не опциональные параметры функции. И самое главное — контекст ничего не отменяет сам: он только закрывает канал, а прекратить работу должен ваш код, который обязан этот канал слушать.

Коротко. context.Context — интерфейс-носитель сигнала отмены, дедлайна и небольшого набора значений, привязанных к одному логическому запросу. Работает он через канал: ctx.Done() возвращает <-chan struct{}, который закрывается при отмене или истечении дедлайна, и все горутины, читающие из этого канала в select, мгновенно просыпаются и завершают работу сами.

Глубже. Внутри лежит cancelCtx со встроенным родителем, мьютексом, лениво создаваемым каналом done (поле atomic.Value, канал аллоцируется только при первом вызове Done()), картой потомков children map[canceler]struct{} и полями err/cause. Вызов cancel под мьютексом ставит err, делает close(done), рекурсивно вызывает cancel у всех детей и вычёркивает себя из children родителя. Именно close, а не отправка в канал: закрытие — единственная операция с каналом, которую «видят» сразу все читатели, и она идемпотентна для читателей (повторное чтение из закрытого канала возвращает нулевое значение немедленно). Повторный вызов cancel безопасен — код проверяет c.err != nil и выходит.

func worker(ctx context.Context, jobs <-chan Job) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled или context.DeadlineExceeded
case j, ok := <-jobs:
if !ok {
return nil
}
handle(ctx, j)
}
}
}

Что такое context.Context ? Для чего используется и какие методы у него есть?

Заголовок раздела «Что такое context.Context ? Для чего используется и какие методы у него есть?»

Коротко. Это интерфейс с четырьмя методами: Deadline() (deadline time.Time, ok bool), Done() <-chan struct{}, Err() error, Value(key any) any. Используется для отмены операций, ограничения их по времени и переноса request-scoped значений через границы API и горутин.

Глубже. Контракт методов:

  • Deadline() — возвращает момент, когда работа должна быть прекращена, и ok == false, если дедлайна нет. Полезен, чтобы заранее решить, стоит ли вообще начинать долгую операцию, или чтобы выставить SetReadDeadline на сокете.
  • Done() — канал, закрывающийся при отмене. Для Background()/TODO() возвращает nil, а чтение из nil-канала блокируется навсегда — то есть кейс case <-ctx.Done() в select для неотменяемого контекста просто никогда не сработает, что и требуется.
  • Err()nil, пока Done() не закрыт; после закрытия — context.Canceled или context.DeadlineExceeded и уже не меняется. DeadlineExceeded реализует net.Error с Timeout() == true, поэтому сетевой код умеет отличать таймаут.
  • Value(key) — линейный поиск по цепочке родителей.

С Go 1.20 добавились context.WithCancelCause и context.Cause(ctx): причина отмены — произвольная ошибка, Err() при этом всё равно вернёт Canceled, а Cause() — вашу ошибку. С Go 1.21 — context.WithoutCancel, context.AfterFunc, WithDeadlineCause/WithTimeoutCause.

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}

Коротко. ACID — набор гарантий транзакции в СУБД: Atomicity (атомарность — транзакция применяется целиком или не применяется вовсе), Consistency (согласованность — транзакция переводит БД из одного корректного состояния в другое, не нарушая инвариантов и ограничений), Isolation (изолированность — параллельные транзакции не видят промежуточных состояний друг друга в объёме, заданном уровнем изоляции), Durability (долговечность — зафиксированные изменения переживают падение процесса и сервера).

Глубже. Вопрос попал в подтему про context только из-за слова «контекст» в формулировке — к пакету context он отношения не имеет. По сути: атомарность в PostgreSQL/MySQL обеспечивается undo-логом и откатом, долговечность — WAL/redo-логом с fsync на коммите (в MySQL это innodb_flush_log_at_trx_commit=1), изолированность — блокировками и/или MVCC. Именно I на практике «размывается»: стандарт SQL описывает уровни Read Uncommitted, Read Committed, Repeatable Read, Serializable, и только последний даёт полную изоляцию; на остальных возможны аномалии (грязное/неповторяющееся чтение, фантомы, write skew). Consistency — самая слабая буква: это ответственность приложения и ограничений схемы, а не движка.

Со стороны Go стоит помнить, что database/sql завязан на контекст: db.BeginTx(ctx, nil), tx.ExecContext(ctx, ...). Отмена контекста посреди транзакции приводит к её откату драйвером, поэтому не стоит использовать тот же короткоживущий контекст для коммита, если он может истечь между последним запросом и Commit().

Context. Types. Purpose a) context.Background() , context.TODO() b) WithDeadline() - termination by time c) WithTimeout() - termination after an interval d) Data storage: key-value e) Cancellation via cancel()

Заголовок раздела «Context. Types. Purpose a) context.Background() , context.TODO() b) WithDeadline() - termination by time c) WithTimeout() - termination after an interval d) Data storage: key-value e) Cancellation via cancel()»

Коротко. Это перечисление всей поверхности пакета: два корневых контекста (Background, TODO), два конструктора с временными ограничениями (WithDeadline — до абсолютного момента, WithTimeout — на длительность), контейнер значений (WithValue) и ручная отмена (WithCancel, возвращающий cancel()).

Глубже. Разберём по пунктам.

a) Background() — пустой корень дерева: Done() возвращает nil, Err()nil, Deadline()ok == false, Value()nil. Используется в main, в инициализации, в тестах, как корень для входящего запроса. TODO() устроен внутри идентично (оба — обёртки над emptyCtx), но семантически означает «здесь должен быть настоящий контекст, но я ещё не протянул его сюда»; линтеры и go vet-подобные инструменты умеют это подсвечивать.

b) WithDeadline(parent, t) — отмена в момент времени t. Если у родителя дедлайн уже раньше t, конструктор вырождается в WithCancel(parent) — новый таймер не создаётся, дедлайн ужесточить можно, ослабить нельзя.

c) WithTimeout(parent, d) — буквально WithDeadline(parent, time.Now().Add(d)), отдельного типа под него нет.

d) WithValue(parent, key, val) — новый узел *valueCtx с ровно одной парой. Каждый вызов — новый узел и новая аллокация; десять значений — цепочка из десяти узлов.

e) WithCancel(parent) возвращает (ctx, cancel). cancel идемпотентен, потокобезопасен и обязателен к вызову — даже если контекст уже отменился по таймауту, defer cancel() нужен, чтобы отцепить узел от родителя и остановить таймер.

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req) // прервётся по ctx

Коротко. Чтобы вовремя прекращать ненужную работу: когда клиент отвалился, когда истёк таймаут, когда сервис останавливается или когда один из параллельных подзапросов уже упал и остальные считать бессмысленно. Плюс — чтобы протащить сквозь весь стек вызовов request-scoped данные вроде trace ID.

Глубже. Экономическая суть: без отмены сервис под нагрузкой продолжает жечь CPU, соединения к БД и память на запросы, результат которых уже никому не нужен, — и это классический механизм каскадной деградации. Контекст даёт единый протокол, понятный всей экосистеме: net/http, database/sql, google.golang.org/grpc, драйверы Redis и Kafka — все принимают ctx и все умеют по нему прерываться. В gRPC дедлайн вообще передаётся по сети (grpc-timeout в заголовках), так что таймаут распространяется между сервисами, а не только внутри процесса.

Коротко. См. выше про «Зачем нужен context.Context»: отмена, дедлайн, передача request-scoped значений. Одной фразой: контекст — это способ сказать всему поддереву горутин «останавливайся», потому что напрямую убить горутину в Go нельзя.

Что такое контекст в Го? Для чего используется?

Заголовок раздела «Что такое контекст в Го? Для чего используется?»

Коротко. Интерфейс context.Context из пакета context — стандартный носитель сигнала отмены, дедлайна и значений запроса, который по соглашению передаётся первым параметром во все функции, способные блокироваться или выполняться долго.

Глубже. Это самый частый вариант вопроса, и хороший ответ строится по трём слоям. Первый — назначение: три задачи (cancel, deadline/timeout, values), из них главная — отмена. Второй — механика: дерево иммутабельных узлов, Done()-канал, который закрывают, каскад вниз, поиск значений вверх. Третий — дисциплина использования: ctx первым аргументом с именем ctx, defer cancel() всегда, в структуры не класть, nil не передавать (для заглушки есть TODO()), значения — только request-scoped и через приватный тип ключа. Если успеть проговорить все три слоя, дальше обычно спрашивают про WithValue и про то, кто именно останавливает работу.

Типовое место, где контекст «появляется на свет» в веб-сервисе: net/http кладёт в каждый *http.Request контекст, который отменяется при разрыве соединения клиентом, и достаётся он через r.Context().

func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=$1", 42)
var name string
if err := row.Scan(&name); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, "internal", http.StatusInternalServerError)
return
}
fmt.Fprintln(w, name)
}

Коротко. Формально — это композиция нескольких: Decorator/Wrapper (каждый With* оборачивает родителя, добавляя одно свойство и делегируя остальные методы), Chain of Responsibility для Value (запрос идёт вверх по цепочке до первого узла, который знает ключ) и Observer/publish-subscribe для отмены (закрытие канала — широковещательное уведомление всех подписчиков).

Глубже. Наиболее точный ответ — декоратор поверх иммутабельного связного списка. Взгляните на определение valueCtx: он встраивает Context (родителя) и переопределяет только Value, а Deadline/Done/Err «проваливаются» в родителя через embedding. То же с cancelCtx. Отсюда же иммутабельность и потокобезопасность на чтение: узел после создания не меняется (кроме внутреннего состояния отмены, защищённого мьютексом), поэтому один и тот же ctx можно спокойно передать в сотню горутин.

Ещё контекст часто называют реализацией паттерна cancellation token (как CancellationToken в .NET) и «неявного параметра запроса» (ambient/request-scoped context). Что контекст точно не реализует — это dependency injection и thread-local storage, хотя его регулярно пытаются использовать в этом качестве.

Коротко. context.TODO() ставят там, где контекст по-хорошему нужен, но его пока неоткуда взять: функция ещё не принимает ctx, идёт постепенная миграция API, или вы не уверены, какой контекст здесь уместен. Это маркер «доделать», а не рабочее значение.

Глубже. Технически TODO() и Background() неотличимы по поведению — оба возвращают обёртку над emptyCtx, у обоих Done() == nil. Отличается только строка в String(): "context.TODO" против "context.Background", что видно в отладочном выводе и в сообщениях go vet/статических анализаторов. Правило простое: Background() — осознанный корень (main, init, тест, фоновый воркер, живущий всё время работы процесса), TODO() — временная заглушка, которую предполагается заменить. Отдельная плохая практика — TODO() внутри обработчика HTTP-запроса вместо r.Context(): так вы теряете отмену при разрыве соединения.

Зачем нужен контекст? Какую задачу решает?

Заголовок раздела «Зачем нужен контекст? Какую задачу решает?»

Коротко. См. выше: контекст решает задачу управляемого завершения работы в конкурентной программе. Ключевой акцент, который стоит добавить: в Go нельзя прервать горутину извне, поэтому нужен кооперативный протокол — общий сигнал, который все участники обязаны периодически проверять.

Глубже. Формулировка «какую задачу решает» подразумевает сравнение с альтернативами. До context каждый писал своё: канал quit chan struct{}, sync.Cond, флаг под мьютексом, time.After в select. Всё это работает, но не композируется: у чужой библиотеки свой способ отмены, и связать их нельзя. Контекст стандартизировал именно композицию — производные контексты складываются в дерево, а таймаут родителя автоматически ограничивает всех потомков. Второй решённой задачей стала передача метаданных запроса без глобальных переменных и без протаскивания десятого параметра через каждый слой (trace/span ID для распределённой трассировки — канонический пример).

Коротко. Неблокирующей проверкой: if ctx.Err() != nil { ... } или select { case <-ctx.Done(): ...; default: }. Первый вариант проще и обычно предпочтительнее. Но точечная проверка нужна редко — правильнее держать case <-ctx.Done() внутри select рядом с блокирующей операцией.

Глубже. Различие важное: ctx.Err() и select ... default — это «моментальный снимок», результат может устареть через наносекунду, поэтому такая проверка полезна только как ранний выход перед началом дорогой работы (в начале итерации цикла, перед походом в БД). Настоящая защита — не опрашивать, а ждать: если ваш код блокируется на канале, сокете или мьютексе, он должен либо использовать функцию с суффиксом Context (ExecContext, NewRequestWithContext), либо сам стоять в select вместе с <-ctx.Done(). Обратите внимание: errors.Is(ctx.Err(), context.Canceled) работает, но обычно достаточно ctx.Err() != nil — эти ошибки сравниваются напрямую, они синглтоны.

for _, item := range items {
if err := ctx.Err(); err != nil {
return err // ранний выход: контекст уже истёк или отменён
}
process(ctx, item)
}

Коротко. Через select: блокирующе — select { case <-ctx.Done(): return ctx.Err(); case v := <-ch: ... }, неблокирующе — тот же select с веткой default. Напрямую <-ctx.Done() вне select пишут только там, где горутина именно ждёт отмены и ничего больше.

Глубже. Три нюанса. Первый: у Background()/TODO() метод возвращает nil-канал, чтение из него блокируется навсегда — это не баг, а корректное поведение «отмены не будет никогда», и select просто никогда не выберет этот кейс. Второй: Done() у cancelCtx создаёт канал лениво и берёт мьютекс на первом вызове, поэтому в горячем цикле лучше один раз сохранить done := ctx.Done() в локальную переменную. Третий: после закрытия канала он готов к чтению всегда, поэтому select с уже отменённым контекстом и другой готовой веткой выберет случайную из двух — если отмена должна иметь приоритет, проверяйте ctx.Err() явно в начале итерации.

done := ctx.Done() // избегаем повторного взятия мьютекса в цикле
for {
select {
case <-done:
return ctx.Err()
case v, ok := <-in:
if !ok {
return nil
}
select {
case out <- v:
case <-done: // отправка тоже должна быть отменяемой
return ctx.Err()
}
}
}

Что такое context.WithTimeout и context.WithCancel ? Примеры использования.

Заголовок раздела «Что такое context.WithTimeout и context.WithCancel ? Примеры использования.»

Коротко. WithCancel(parent) возвращает производный контекст и функцию cancel(), вызов которой отменяет его вручную. WithTimeout(parent, d) возвращает контекст, который дополнительно отменится сам через d (внутри — WithDeadline(parent, time.Now().Add(d))). Оба возвращают cancel, и оба требуют его вызвать — обычно defer cancel().

Глубже. WithCancel типичен для fan-out: запустили несколько горутин, первая же ошибка отменяет остальные (ровно это делает errgroup.WithContext из golang.org/x/sync). WithTimeout — для любого внешнего I/O: HTTP-вызов, запрос к БД, обращение в кэш. Важно, что cancel() у WithTimeout нужен не «на всякий случай»: он останавливает time.Timer и удаляет узел из children родителя. Без него при долгоживущем родительском контексте (например, контекст фонового воркера на весь процесс) карта children растёт неограниченно — это утечка памяти, которую go vet частично ловит проверкой lostcancel.

// WithCancel: гонка за первым результатом, остальные отменяем.
func first(ctx context.Context, urls []string) (string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // отменит оставшиеся запросы при выходе
type res struct {
body string
err error
}
ch := make(chan res, len(urls)) // буфер, чтобы проигравшие не залипли
for _, u := range urls {
go func(u string) {
b, err := fetch(ctx, u)
ch <- res{b, err}
}(u)
}
for range urls {
r := <-ch
if r.err == nil {
return r.body, nil // defer cancel() свернёт остальных
}
}
return "", errors.New("all failed")
}
// WithTimeout: жёсткий бюджет на внешний вызов.
func fetch(ctx context.Context, url string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
return string(b), err
}

Коротко. См. выше: интерфейс из пакета context с методами Deadline, Done, Err, Value, реализующий стандартный протокол отмены и передачи данных запроса между горутинами.

Глубже. Если формулировка вопроса совсем короткая, интервьюер чаще всего проверяет, не путаете ли вы контекст с чем-то другим. Полезно сразу отграничить: это не механизм хранения состояния приложения, не DI-контейнер, не аналог thread-local, и не «объект запроса». Это носитель сигнала времени жизни операции.

Коротко. По способу создания: пустые корневые (Background, TODO) и производные — отменяемый (WithCancel, WithCancelCause), с дедлайном (WithDeadline, WithTimeout и их *Cause-варианты), со значением (WithValue) и отвязанный от отмены родителя (WithoutCancel, Go 1.21+). Внутри пакета им соответствуют типы backgroundCtx/todoCtx, *cancelCtx, *timerCtx, *valueCtx, withoutCancelCtx.

Глубже. Иерархия реализаций: emptyCtx — база для backgroundCtx и todoCtx; cancelCtx встраивает родительский Context и добавляет done/children/err/cause; timerCtx встраивает cancelCtx и добавляет *time.Timer и deadline, то есть таймаут — это отмена плюс таймер; valueCtx встраивает родителя и добавляет пару key, val.

Из свежего: context.WithoutCancel(parent) (Go 1.21) даёт контекст, который наследует значения родителя, но не наследует его отмену и дедлайн — нужен, когда после ответа клиенту надо доделать фоновую работу (записать метрику, отправить событие), не потеряв trace ID. context.AfterFunc(ctx, f) (Go 1.21) запускает f в отдельной горутине после отмены ctx и возвращает stop для отписки — удобная замена ручному go func(){ <-ctx.Done(); ... }(). context.WithCancelCause (Go 1.20) позволяет приложить к отмене причину, доступную через context.Cause(ctx). И в Go 1.24 в testing появился t.Context() — контекст теста, который отменяется перед запуском функций из t.Cleanup.

ctx, cancel := context.WithCancelCause(context.Background())
cancel(fmt.Errorf("upstream 503"))
fmt.Println(ctx.Err()) // context canceled
fmt.Println(context.Cause(ctx)) // upstream 503

Коротко. Для переноса request-scoped данных через границы API и горутин: trace/correlation ID, идентификатор аутентифицированного пользователя, локаль, флаг «запрос от админки». Не для передачи опциональных параметров функции и не для хранения зависимостей (логгер, пул соединений — это поля структуры, а не контекст).

Глубже. Обязательное правило — приватный неэкспортируемый тип ключа, иначе два пакета с ключом-строкой "user" перетрут друг друга, а внешний код сможет подменить ваше значение. WithValue паникует, если ключ nil или несравним. Стоимость: Value(key) — линейный проход по цепочке (см. функцию value в исходниках), каждый WithValue — отдельная аллокация узла; класть в контекст 15 значений и читать их в горячем цикле — плохая идея. Типобезопасность обеспечивается парой геттер/сеттер, а не прямым использованием ctx.Value в бизнес-коде.

package auth
type ctxKey struct{} // приватный тип — коллизии невозможны
type User struct{ ID int64 }
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, ctxKey{}, u)
}
func UserFrom(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(ctxKey{}).(*User)
return u, ok
}

Что такое контекст и как его можно использовать?

Заголовок раздела «Что такое контекст и как его можно использовать?»

Коротко. См. выше про определение. Использование сводится к четырём сценариям: (1) таймаут на внешний вызов — WithTimeout + функция с суффиксом Context; (2) отмена группы горутин при первой ошибке — WithCancel или errgroup.WithContext; (3) graceful shutdown — контекст, отменяемый по SIGTERM через signal.NotifyContext; (4) передача trace ID через WithValue.

Глубже. Пример graceful shutdown, который часто просят написать:

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done() // пришёл сигнал
stop() // возвращаем стандартную обработку сигнала
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
}

Обратите внимание на деталь: контекст для Shutdown создаётся от Background(), а не от уже отменённого ctx, иначе завершение прервётся мгновенно.

Коротко. Да, это один из канонических примеров request-scoped значения — при условии, что userID туда кладёт middleware аутентификации, ключ — приватный тип, а доступ идёт через типизированные WithUser/UserFrom. Оговорка, которую стоит проговорить: бизнес-логика не должна зависеть от того, есть ли значение в контексте, — если функции обязательно нужен ID пользователя, это явный параметр.

Глубже. Граница проходит по слову «обязательный». Официальная документация формулирует так: контекст несёт «request-scoped data that transits process and API boundaries», а не опциональные параметры. userID, извлечённый из JWT в middleware, границу пересекает — и его класть можно. А вот userID, от которого зависит результат функции GetOrders(ctx), лучше сделать аргументом GetOrders(ctx, userID): сигнатура становится честной, функция тестируется без конструирования контекста, и компилятор ловит забытый параметр вместо паники в рантайме на неудачном type assertion. Компромисс, который используют многие команды: в контексте — «личность вызывающего» (для аудита, логов, авторизации в middleware), в аргументах — «над чьими данными работаем».

Какие виды Context существуют в Go и для каких задач они применяются?

Заголовок раздела «Какие виды Context существуют в Go и для каких задач они применяются?»

Коротко. См. выше «Какие контексты бывают?». Кратко по задачам: Background — корень в main/тестах/фоновых воркерах; TODO — заглушка при миграции; WithCancel — ручная отмена группы горутин; WithTimeout/WithDeadline — бюджет времени на I/O; WithValue — trace ID и данные аутентификации; WithoutCancel — фоновая доработка после ответа клиенту; WithCancelCause — отмена с диагностируемой причиной.

Глубже. Отличие этого вопроса от предыдущего — акцент на выборе. Практический критерий: сначала спросите, что ограничиваем. Ограничиваем время внешнего вызова — WithTimeout. Ограничиваем время всей операции, включающей несколько шагов, — WithDeadline от одного момента, чтобы шаги делили общий бюджет, а не получали по полному таймауту каждый. Прекращаем по событию, а не по времени, — WithCancel. Нужно и то и другое — комбинируйте: WithCancel(WithTimeout(...)) работает, отменится по первому сработавшему условию.

Что такое контекст? Как работает context.Done? Как бы ты реализовал context.Done?

Заголовок раздела «Что такое контекст? Как работает context.Done? Как бы ты реализовал context.Done?»

Коротко. Done() возвращает канал <-chan struct{}, который закрывается при отмене; сам канал создаётся лениво при первом вызове и хранится в atomic.Value под мьютексом. Реализовал бы так же: канал + sync.Once/мьютекс вокруг close, потому что закрытие канала — единственный дешёвый способ разбудить произвольное число ожидающих одновременно.

Глубже. В стандартной библиотеке cancelCtx.Done() делает быструю проверку c.done.Load(), и только если канала ещё нет — берёт мьютекс, перепроверяет и создаёт make(chan struct{}). Ленивость важна: WithValue-цепочки и контексты, у которых Done() никто не спрашивает, не платят за аллокацию канала. Отдельная микрооптимизация в исходниках — глобальный closedchan, уже закрытый канал, который отдаётся в случаях, когда контекст создан уже отменённым.

Собственная минимальная реализация — хороший ответ на «как бы ты сделал»:

type myCtx struct {
context.Context // родитель: даёт Deadline и Value
done chan struct{}
once sync.Once
mu sync.Mutex
err error
}
func newMyCtx(parent context.Context) (*myCtx, func()) {
c := &myCtx{Context: parent, done: make(chan struct{})}
// распространение отмены сверху вниз
if pd := parent.Done(); pd != nil {
go func() {
select {
case <-pd:
c.cancel(parent.Err())
case <-c.done:
}
}()
}
return c, func() { c.cancel(context.Canceled) }
}
func (c *myCtx) cancel(err error) {
c.once.Do(func() {
c.mu.Lock()
c.err = err
c.mu.Unlock()
close(c.done) // широковещательный сигнал всем читателям
})
}
func (c *myCtx) Done() <-chan struct{} { return c.done }
func (c *myCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}

Разница с настоящим cancelCtx: стандартная реализация не поднимает горутину-наблюдателя, если родитель — тоже *cancelCtx (тогда потомок просто регистрируется в parent.children), горутина остаётся крайним случаем для сторонних реализаций интерфейса — а с Go 1.21 и он покрыт через опциональный метод AfterFunc. Именно поэтому «своя» реализация выше принципиально дороже: горутина на каждый производный контекст.

Коротко. См. выше: context.Context — интерфейс-носитель сигнала отмены, дедлайна и request-scoped значений; фактически стандартный «токен отмены» Go, передаваемый первым аргументом.

Коротко. Только формой задания момента отмены: WithDeadline принимает абсолютное время time.Time, WithTimeout — относительную длительность time.Duration. Реализационно WithTimeout(parent, d) — это ровно WithDeadline(parent, time.Now().Add(d)), отдельного типа под таймаут нет.

Глубже. Практическая разница проявляется, когда общий бюджет надо разделить между несколькими шагами. Если на всю операцию есть 1 секунда и внутри три последовательных вызова, WithTimeout(ctx, time.Second) на каждом даёт до трёх секунд суммарно, а один WithDeadline(ctx, start.Add(time.Second)), переданный во все три, честно ограничивает всю цепочку. Дедлайн ещё естественно ложится на распределённые системы: сервис A получил дедлайн, вычел сетевую задержку и передал остаток сервису B (gRPC делает это автоматически). И ещё: WithDeadline не может ослабить дедлайн родителя — если у родителя срок раньше, конструктор вырождается в WithCancel(parent) и таймер вообще не создаётся.

deadline := time.Now().Add(time.Second)
ctx, cancel := context.WithDeadline(parent, deadline)
defer cancel()
// все три шага делят один бюджет
if err := step1(ctx); err != nil { return err }
if err := step2(ctx); err != nil { return err }
return step3(ctx)

Чем отличается context.Background от context.TODO? (внутри ничем)

Заголовок раздела «Чем отличается context.Background от context.TODO? (внутри ничем)»

Коротко. Поведением — ничем: оба возвращают пустой контекст без отмены, дедлайна и значений (backgroundCtx и todoCtx — две обёртки над одним emptyCtx). Различие чисто семантическое и видно только в String(): Background — осознанный корень дерева, TODO — маркер «сюда надо протянуть настоящий контекст».

Глубже. До Go 1.21 оба буквально были одним типом *emptyCtx с разными значениями-переменными, и context.Background() == context.TODO() было бы false только из-за разных адресов. В Go 1.21 их разделили на два именованных типа, чтобы String() печатал корректное имя и чтобы функция value() могла дёшево отсечь оба на выходе из цикла. Практический смысл различия — для читателя кода и для линтеров: увидев TODO() в ревью, вы понимаете, что здесь недоделка; увидев Background() в глубине бизнес-логики, понимаете, что кто-то оборвал цепочку отмены (и это почти всегда баг — кроме сознательных случаев вроде контекста для srv.Shutdown).

Пакет context , какие бывают, для чего нужны, какие есть методы?

Заголовок раздела «Пакет context , какие бывают, для чего нужны, какие есть методы?»

Коротко. Пакет даёт интерфейс Context с четырьмя методами (Deadline, Done, Err, Value), два корня (Background, TODO), конструкторы производных (WithCancel, WithCancelCause, WithDeadline, WithDeadlineCause, WithTimeout, WithTimeoutCause, WithValue, WithoutCancel), утилиту Cause и хук AfterFunc, плюс две ошибки-синглтона Canceled и DeadlineExceeded и тип CancelFunc.

Глубже. Сводная таблица поверхности пакета на Go 1.24:

ЭлементЧто делаетС какой версии
Background(), TODO()пустые корни1.7
WithCancel(parent) (Context, CancelFunc)ручная отмена1.7
WithDeadline(parent, t) / WithTimeout(parent, d)отмена по времени1.7
WithValue(parent, key, val)одна пара ключ-значение1.7
Canceled, DeadlineExceededошибки из Err()1.7
WithCancelCause, Cause(ctx)отмена с причиной1.20
WithoutCancel(parent)значения без отмены1.21
AfterFunc(ctx, f) (stop func() bool)колбэк после отмены1.21
WithDeadlineCause, WithTimeoutCauseтаймаут с причиной1.21

Важная деталь про Cause: у WithTimeoutCause причину ставит именно срабатывание таймаута, а возвращаемый cancel причину не устанавливает — при ручной отмене Cause() вернёт context.Canceled. И Cause(ctx) для контекста из WithoutCancel вернёт nil, даже если исходный родитель отменён.

Коротко. См. выше. Сжатая формулировка для собеседования: контекст — это объект времени жизни операции; он отвечает на вопрос «нужно ли ещё делать эту работу» и несёт данные, привязанные к запросу, а не к процессу.

Что такое fork в контексте операционной системы?

Заголовок раздела «Что такое fork в контексте операционной системы?»

Коротко. fork(2) — системный вызов POSIX, создающий новый процесс копированием вызывающего: потомок получает копию адресного пространства, дескрипторов и состояния родителя. Возвращает 0 в потомке и PID потомка в родителе; отсюда классическая идиома «fork + exec» — потомок сразу заменяет свой образ через execve.

Глубже. К пакету context вопрос отношения не имеет, попал сюда из-за слова «контекст». Ключевые детали: копирование памяти делается лениво через copy-on-write — страницы помечаются read-only и физически дублируются только при записи. Потомок наследует открытые файловые дескрипторы (с общим смещением в файле), рабочий каталог, umask, обработчики сигналов (сбрасываются в default при exec), но не наследует потоки — в потомке остаётся только тот поток, который вызвал fork. Отсюда классическая проблема async-signal-safety: между fork и exec в многопоточной программе можно безопасно вызывать очень ограниченный набор функций, потому что мьютексы, захваченные другими потоками, останутся захваченными навсегда.

Именно поэтому в Go нет обёртки над голым fork: рантайм многопоточный по своей природе (M:N-планировщик, GC-воркеры, sysmon), и форкнутый процесс окажется в неконсистентном состоянии. Вместо этого стандартная библиотека даёт os/exec и os.StartProcess, которые под капотом делают fork+exec атомарно (на Linux — через clone/vfork в syscall.forkExec, с аккуратной блокировкой на время развилки). Если процесс, ставший зомби, не пожать через wait4, он останется в таблице процессов — os/exec делает это за вас в cmd.Wait().

Что такое контекст и зачем он используется?

Заголовок раздела «Что такое контекст и зачем он используется?»

Коротко. См. выше. Финальная сводка: context.Context — стандартный интерфейс Go для (1) кооперативной отмены дерева горутин, (2) ограничения операций по времени и (3) переноса request-scoped значений. Используется во всей экосистеме — net/http, database/sql, gRPC, драйверы очередей — как первый параметр любой потенциально долгой функции.

  • Говорят «контекст убивает горутину». Не убивает: он только закрывает канал Done(), а завершиться горутина должна сама, проверив сигнал. Если код не слушает ctx.Done() и не использует *Context-функции, отмена не сделает ничего.
  • Забывают defer cancel() и не могут объяснить, что именно течёт. Течёт узел в children родителя и активный time.Timer; при долгоживущем родителе это растущая память, а не только «лишний таймер».
  • Путают Background и TODO, называя между ними поведенческое различие. Внутри они идентичны, разница только семантическая и в String().
  • Кладут в контекст всё подряд: логгер, пул БД, конфиг, обязательные параметры функции. Контекст — только для request-scoped данных, пересекающих границы API; зависимости — поля структуры.
  • Используют строку как ключ WithValue (ctx.Value("user")) — это гарантированная коллизия между пакетами. Ключ должен быть значением приватного неэкспортируемого типа.
  • Хранят ctx полем структуры и переиспользуют его между вызовами. Контекст привязан к одной операции; поле в структуре ломает и отмену, и трассировку. Исключение — структуры, которые сами моделируют один запрос.
  • Считают, что WithTimeout даёт «мягкий» таймаут и функция обязательно вернётся вовремя. Если внутри есть неотменяемый вызов (тяжёлый CPU-цикл, os.ReadFile, чужая библиотека без ctx), контекст истечёт, а функция продолжит работать.
  • Проверяют только ctx.Err() != nil в начале функции и считают задачу решённой — это снимок состояния, а не ожидание; для блокирующих операций нужен select с <-ctx.Done().
  • Не различают context.Canceled и context.DeadlineExceeded при обработке ошибок, из-за чего таймаут превращается в 500 вместо 504, а отмена клиентом попадает в алерты как ошибка сервиса.