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