← назад к разделу

GO КАК ФУНДАМЕНТ Context и отмена

Обязательно

Context и отмена в Go

Go

Дедлайны, отмена операций, значения запроса и проброс контекста до самого ввода-вывода.

1. Context связывает работу запроса

1 / 5

Контекст передаёт отмену, дедлайн и значения запроса по цепочке вызовов. Один объект сопровождает операцию от начала до конца.

  • Не храните его в структуре долгой жизни.
  • Передавайте первым параметром.
  • Используйте context.Background только на границе.
requesthandlerdb + APIctx идёт тем же путём

2. Дедлайн ограничивает ожидание

2 / 5

WithTimeout и WithDeadline отменяют операцию автоматически. Освобождение контекста через defer cancel обязательно.

  • Таймаут ограничивает конкретную работу.
  • HTTP-клиент получает тот же контекст.
  • После дедлайна проверяйте ctx.Err().
WithTimeoutctx.Donestopdefer cancel()

3. Отмену проверяют в блокирующих местах

3 / 5

select между работой и ctx.Done() позволяет выйти. Просто создать контекст недостаточно.

  • Проверяйте отмену в цикле и ожидании канала.
  • Освобождайте воркеры и соединения.
  • После отмены новые задачи не запускаются.
selectresultctx.Donereturn

4. Значения относятся к запросу

4 / 5

Request id и параметры трассировки можно передавать через context.WithValue. Бизнес-данные и зависимости там не живут.

  • Ключ должен быть собственным типом.
  • Значение доступно через ctx.Value(key).
  • Не используйте context как глобальную map.
WithValue(requestID)handlerlogsМетаданные запроса, не зависимости

5. Цепочку нельзя обрывать

5 / 5

Главная ошибка - вызвать context.Background внутри операции. Тогда отмена клиента и дедлайн перестают работать.

  • Каждый слой принимает родительский ctx.
  • WithoutCancel только для отдельной фоновой работы.
  • AfterFunc помогает освободить ресурс.
parent ctxchild ctxIOBackground внутри = потерянная отмена

← назад к разделу

В большинстве языков нет стандартного способа сказать работающей операции «всё, стоп, отменяй». Пользователь закрыл соединение — сервер продолжает запрос в базу. Таймаут вышел — горутины зависли и ждут. context.Context в Go — это стандартный механизм, который решает именно эту проблему.

Что такое context

Контекст — это небольшой объект, который переходит с запросом от начала до конца. Он несёт две вещи:

  • сигнал отмены — когда надо прекратить работу;
  • значения запроса — например, кто сделал запрос и с каким ID.

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

Соглашение Go: ctx context.Context всегда идёт первым аргументом функции, которая делает что-то долгое или отменяемое.

func (h *OrderHandler) Handle(ctx context.Context, cmd CreateOrderCommand) (Order, error) {
    return h.repo.Save(ctx, cmd)
}

Так контекст путешествует с запросом через все слои — от обработчика до базы данных.

Откуда берётся контекст

В HTTP-сервере каждый входящий запрос уже имеет свой контекст:

func (h *OrderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // контекст запроса уже готов
    order, err := h.handle(ctx, r)
    // ...
}

Этот контекст живёт ровно столько, сколько длится запрос. Если клиент закрывает соединение — r.Context() отменяется, и отмена распространяется на всё, что получило этот ctx.

Когда нет входящего запроса (например, в фоновой горутине при старте приложения), используют context.Background() — пустой корневой контекст без отмены.

Таймауты и дедлайны

Самое частое применение — ограничить время операции. Без таймаута один медленный запрос в базу может держать горутину вечно.

context.WithTimeout создаёт дочерний контекст, который автоматически отменится через заданное время:

func (h *ReportHandler) Handle(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    return h.db.RunSlowQuery(ctx)
}

defer cancel() обязателен — он освобождает ресурсы контекста, даже если операция завершилась раньше таймаута. Пропустить cancel() — значит держать таймер и память дочернего контекста до тех пор, пока не сработает его собственный таймаут, а для WithCancel — пока не отменится родитель. go vet ловит такие места проверкой lostcancel.

context.WithDeadline работает так же, но принимает конкретный момент времени:

deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()

Проверка отмены

Когда операция долгая (например, цикл обработки), нужно периодически проверять, не отменили ли контекст:

func processItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        select {
        case <-ctx.Done():
            return ctx.Err() // context.Canceled или context.DeadlineExceeded
        default:
            process(item)
        }
    }
    return nil
}

ctx.Done() — канал, который закрывается при отмене. ctx.Err() возвращает причину: context.Canceled (кто-то вызвал cancel()) или context.DeadlineExceeded (вышло время).

Значения в контексте

В контекст можно положить данные, связанные с запросом, — идентификатор запроса, текущего пользователя. Это удобно, когда нужно передать данные через несколько слоёв без изменения сигнатур функций.

Ключ должен быть своим приватным типом — не строкой. Это исключает коллизии между пакетами: два пакета могут использовать одну строку "user", но не смогут случайно использовать один тип.

type ctxKey int

const userKey ctxKey = iota

func WithUser(ctx context.Context, u User) context.Context {
    return context.WithValue(ctx, userKey, u)
}

func UserFrom(ctx context.Context) (User, bool) {
    u, ok := ctx.Value(userKey).(User)
    return u, ok
}

Так middleware аутентификации кладёт пользователя в контекст, а обработчик достаёт его без лишних аргументов.

Важное ограничение: контекст — для данных запроса (кто, какой запрос, trace ID), а не для передачи бизнес-параметров. Если данные нужны только паре функций — лучше передать их напрямую через аргументы.

Проброс до конца

Сила контекста в том, что отмена доходит до самого дна — если ctx пробрасывается везде. Современные библиотеки работы с базами данных принимают ctx в каждый запрос:

func (r *OrderRepository) Save(ctx context.Context, order Order) error {
    _, err := r.pool.Exec(ctx,
        `INSERT INTO orders (id, amount) VALUES ($1, $2)`,
        order.ID, order.Amount,
    )
    return err
}

Если клиент закрыл соединение, ctx отменится, и база данных тоже прекратит работу — запрос не будет зря тратить ресурсы.

Главная ошибка — обрыв цепочки

Самая распространённая ошибка — заменить пришедший ctx на context.Background() где-то в середине:

// неправильно: отмена и таймаут до базы не дойдут
func (r *OrderRepository) Save(ctx context.Context, order Order) error {
    _, err := r.pool.Exec(context.Background(), // обрыв цепочки!
        `INSERT INTO orders ...`, order.ID)
    return err
}

После такого обрыва запрос в базу продолжится, даже если клиент давно ушёл и таймаут вышел.

Правило: пробрасывай тот ctx, что получил. Свой новый контекст создавай только когда добавляешь таймаут, дедлайн или значение.

Ручная отмена

Иногда нужно отменить операцию по логике, а не по таймауту. context.WithCancel даёт cancel-функцию, которую можно вызвать явно:

ctx, cancel := context.WithCancel(ctx)
defer cancel()

go func() {
    // если что-то пошло не так — отменяем всё
    if somethingWentWrong {
        cancel()
    }
}()

Это удобно для координации нескольких горутин: одна заметила проблему и отменила контекст — остальные тоже остановятся.

Дополнительно: при первом чтении можно пропустить

Глубже: WithoutCancel, Cause и AfterFuncрасширенное

Правило «не подменяй ctx на Background» имеет одно честное исключение. После того как ответ клиенту отправлен, иногда надо доделать работу, которая не должна умереть вместе с запросом: записать аудит, отправить событие. Раньше для этого писали context.Background() и теряли значения запроса — идентификатор, пользователя. С Go 1.21 есть context.WithoutCancel(ctx): он сохраняет все значения родителя, но не наследует его отмену и дедлайн — ровно то, что нужно для «доделать после ответа».

ctx.Err() говорит только «отменён» или «вышло время». Если причин отмены несколько, их различают через context.WithCancelCause: cancel(errors.New("превышен лимит")), а на другом конце context.Cause(ctx) вернёт эту ошибку вместо безликого context.Canceled. И context.AfterFunc(ctx, f) запускает f в момент отмены — удобно закрыть соединение или снять блокировку, не заводя ради этого отдельную горутину с select.

Коротко

  • context.Context несёт сигнал отмены и данные запроса; контексты образуют дерево — отмена родителя отменяет всех потомков.
  • ctx всегда первый аргумент функции; получают из r.Context() или context.Background().
  • context.WithTimeout / context.WithDeadline — таймаут и дедлайн; defer cancel() обязателен.
  • ctx.Done() — канал отмены; ctx.Err() — причина (Canceled или DeadlineExceeded).
  • Ключи для context.WithValue — приватный тип, не строка; контекст для данных запроса, не для бизнес-параметров.
  • Нельзя обрывать цепочку: передавать context.Background() вместо пришедшего ctx — отмена и таймаут не дойдут до базы.
  • Доделать работу после ответа, не теряя значений запроса, — context.WithoutCancel; причину отмены различают через WithCancelCause и context.Cause.

Что пощупать

В практикуме remodov/marketplace-system-go context.Context идёт сквозь все слои: signal.NotifyContext в main.go гасит сервер по SIGTERM, обработчик берёт r.Context(), а pgx получает его в каждом запросе и транзакции.

Код: main.go, service.go.

Что почитать дальше

  • Middleware в Go — как middleware кладёт данные в контекст.
  • Persistence и sqlc в Go — как pgx принимает контекст в каждый запрос к базе.