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

В большинстве языков нет стандартного способа сказать работающей операции «всё, стоп, отменяй». Пользователь закрыл соединение — сервер продолжает запрос в базу. Таймаут вышел — горутины зависли и ждут. 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() — значит держать память до тех пор, пока не сработает таймаут родителя.

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()
    }
}()

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

Коротко

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

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

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