context.Context — то, чего нет в магических фреймворках и что пронизывает любой Go-сервис. Это единый канал, по которому запрос несёт с собой две вещи: связанные с ним значения и сигнал отмены. Понять context — значит понять, как в Go отменяют работу, ставят таймауты и доносят данные запроса до самого дна вызова.

Что такое context

Каждый HTTP-запрос несёт свой контекст, доступный через r.Context(). Контексты образуют дерево: из родительского порождают дочерние, и отмена родителя отменяет всех потомков. Соглашение Go железное: ctx context.Context идёт первым аргументом функции, которая делает что-то отменяемое или долгое.

func (h *CreateProductHandler) Handle(ctx context.Context, cmd CreateProductCommand) (Product, error) {
    return h.repo.Save(ctx, cmd)
}

Значения запроса

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

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 аутентификации кладёт пользователя в контекст, а обработчик достаёт. Контекст — для данных запроса, не для передачи опциональных параметров функций: туда кладут то, что сквозное (кто, какой запрос), а не бизнес-аргументы.

Дедлайны и отмена

Контекст несёт отмену — и это спасает от зависших запросов и утечки ресурсов. Таймаут на операцию ставят context.WithTimeout.

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

defer cancel() обязателен — он освобождает ресурсы контекста. Если клиент разорвёт соединение, net/http отменит r.Context(), и отмена дойдёт до всех, кто принял этот ctx.

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

Сила context в том, что отмена доходит до самого дна — если ctx пробрасывается везде. База (pgx) принимает ctx в каждый запрос; HTTP-клиент к соседнему сервису — тоже. Тогда отменённый запрос не продолжает зря молотить в базе.

func (r *ProductRepository) Save(ctx context.Context, cmd CreateProductCommand) (Product, error) {
    row := r.pool.QueryRow(ctx, `INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id`,
        cmd.Name, cmd.Price)
    // ...
}

Главная ошибка — оборвать цепочку: передать context.Background() в середине вместо пришедшего ctx. Тогда отмена и дедлайн до базы не дойдут. Правило простое: пробрасывай тот ctx, что получил, а свой создавай только когда добавляешь таймаут или значение.

Где это в UCP

Context — это инфраструктурная нить, проходящая сквозь все слои UCP: обработчик берёт r.Context(), Handler принимает его первым аргументом, репозиторий отдаёт в pgx. Бизнес-логика не зависит от context — она лишь пробрасывает его дальше. У магических фреймворков отмена и request-scope спрятаны; Go делает их явными, и продукт-инженер точно знает, докуда дойдёт отмена и что несёт запрос. Это явный аналог request-scope и таймаутов, которые Spring прячет за абстракциями.