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 прячет за абстракциями.