---
title: "context"
description: "Сервис обрабатывает HTTP-запрос: идёт в базу, вызывает два соседних сервиса, что-то считает в фоновых горутинах. Клиент закрыл вкладку. Вся эта работа больше никому не нужна, но…"
canonical: https://graphlms.ru/go/book/context
course: "Конкурентность Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# context

> Источник: https://graphlms.ru/go/book/context — курс «Конкурентность Go», GraphLMS.

## О чём эта глава

Сервис обрабатывает HTTP-запрос: идёт в базу, вызывает два соседних сервиса, что-то
считает в фоновых горутинах. Клиент закрыл вкладку. Вся эта работа больше никому не
нужна, но продолжает выполняться: держит соединения с базой, занимает процессор. Нужен
способ одним действием сказать всем участникам «заканчиваем».

В Go таким способом служит `context.Context`. Он решает три задачи: **отмена**
(остановить работу досрочно), **дедлайны** (не ждать дольше заданного срока) и
**передача данных запроса** (идентификатор запроса, данные трассировки) через все слои
вызовов.

Устроен контекст на уже знакомой идее: сигнал отмены — это закрытие канала, которое
видят сразу все ожидающие (см. [главу о каналах](/go/book/channels)). Context добавляет к
этому иерархию: отмена родителя отменяет всех потомков.

## Перед чтением

Нужны `select`, закрытие канала как сигнал, функции как значения и возврат `error`.

`context.Context` — интерфейс с четырьмя методами. Два из них нужны постоянно:
`ctx.Done()` возвращает канал, который закрывается при отмене, а `ctx.Err()` объясняет
причину. Функции `WithCancel`, `WithTimeout` и `WithDeadline` возвращают вместе с новым
контекстом функцию `cancel`; её вызывают как обычную функцию, `cancel()`. Она сообщает
об отмене и сразу возвращается, не дожидаясь, пока работа действительно остановится.

Два термина. **Дедлайн** — момент времени, после которого операция должна прекратить
ожидание («до 12:00:05»). **Таймаут** — длительность, из которой дедлайн вычисляют
(«5 секунд от сейчас»).

На первом проходе изучите `WithCancel`, `WithTimeout`, проверку `Done` и обязательный
`cancel`. Разделы про HTTP, `WithValue`, новые функции пакета и сигналы ОС можно
перечитать позже.

## Соглашение: ctx — первый аргумент

Во всём Go-коде принято одно соглашение: контекст передают первым параметром с именем
`ctx`:

```go
func FetchUser(ctx context.Context, id int) (*User, error) { ... }
```

И несколько правил, за нарушение которых придирается и линтер, и ревьюер:

- Не передавайте `nil` вместо контекста. Если контекста пока нет, используйте
  `context.TODO()`: он ничего не делает и только помечает место, куда позже нужно
  протащить настоящий.
- Не храните контекст в поле структуры. Контекст относится к одной операции, а
  структура живёт дольше. Передавайте его аргументом.
- В HTTP-обработчике контекст уже есть: `r.Context()`. Сервер отменяет его, когда клиент
  разрывает соединение или обработчик возвращается.

```go
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()                     // отменится, если клиент уйдёт
    rows, err := db.QueryContext(ctx, q)   // база прервёт запрос по отмене
    // ...
}
```

Остаётся передать этот `ctx` во все вызовы ниже: в базу, в исходящие HTTP-запросы, в
свои функции.

## Дерево отмены

В корне любого дерева — `context.Background()`: пустой контекст, который никогда не
отменяется сам. От любого контекста можно создать дочерний:

```go
ctx, cancel := context.WithCancel(parent)                 // отмена вручную
ctx, cancel := context.WithTimeout(parent, 5*time.Second) // плюс отмена через 5 с
ctx, cancel := context.WithDeadline(parent, deadline)     // плюс отмена в момент deadline
defer cancel() // вызывать обязательно; почему — ниже
```

Главное свойство дерева: **отмена родителя отменяет всех потомков, но не наоборот.**

- Отменили родителя — закрылись каналы `Done()` у детей, внуков и всех ниже.
- Отменили ребёнка — родитель и его остальные дети продолжают работать.

`WithTimeout(parent, d)` — то же самое, что `WithDeadline(parent, time.Now().Add(d))`.
Когда дедлайн наступает, контекст отменяется сам, как будто кто-то вызвал `cancel`.

Дочерний контекст не может жить дольше родителя. Если у родителя до дедлайна осталось
50 мс, а вы создали ребёнка с таймаутом 200 мс, ребёнок всё равно будет отменён через
50 мс вместе с родителем.

## Done() и проверка отмены

Код узнаёт об отмене через `ctx.Done()`: канал, который закрывается в момент отмены.
Закрытый канал всегда готов к приёму, поэтому ветка `<-ctx.Done()` в
[select](/go/book/select) срабатывает сразу после отмены:

```go
func worker(ctx context.Context, in <-chan Job) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()        // Canceled или DeadlineExceeded
        case job, ok := <-in:
            if !ok {
                return nil          // задачи кончились
            }
            process(job)
        }
    }
}
```

После отмены `ctx.Err()` возвращает причину:

- `context.Canceled` — кто-то вызвал `cancel()`;
- `context.DeadlineExceeded` — истёк срок.

До отмены `ctx.Err()` возвращает `nil`. `ctx.Deadline()` сообщает дедлайн, если он задан;
по нему можно решить, есть ли смысл начинать долгую операцию.

Важно понимать, чего context **не** делает. Он не прерывает выполняющийся код. Если
функция десять секунд считает в цикле и ни разу не смотрит на `ctx.Done()`, отмена на
неё не подействует: она досчитает до конца. Отмена в Go кооперативная: код сам
проверяет сигнал в удобных местах и сам сворачивается.

Воркер обрабатывает задачи, пока его не отменят:

```go play
package main

import (
	"context"
	"fmt"
	"sync"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	jobs := make(chan int)
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			select {
			case <-ctx.Done():
				fmt.Println("воркер остановлен:", ctx.Err())
				return
			case j := <-jobs:
				fmt.Println("обработал задачу", j)
			}
		}
	}()

	for i := 1; i <= 3; i++ {
		jobs <- i // отправка завершится, когда воркер примет задачу
	}
	cancel()
	wg.Wait()
	fmt.Println("main завершился")
}
```

```text
обработал задачу 1
обработал задачу 2
обработал задачу 3
воркер остановлен: context canceled
main завершился
```

Порядок вывода здесь гарантирован, и стоит понять почему. Канал `jobs` небуферизованный:
каждая отправка в `main` завершается только тогда, когда воркер принял задачу. После
третьей отправки `main` вызывает `cancel()`. В этот момент в `jobs` ничего нет, готова
только ветка `ctx.Done()`, и воркер выходит. Если бы `jobs` был буферизованным и
задачи лежали в буфере в момент отмены, обе ветки оказались бы готовы, и `select` мог бы
выбрать любую: часть задач осталась бы необработанной.

## Таймаут вместо ручной отмены

Часто отменять руками не нужно: достаточно сказать «жди не дольше N». Это `WithTimeout`.
Функция `call` ждёт либо ответа, либо отмены контекста:

```go play
package main

import (
	"context"
	"fmt"
	"time"
)

func call(ctx context.Context, reply <-chan string) string {
	select {
	case r := <-reply:
		return "ответ: " + r
	case <-ctx.Done():
		return "не дождались: " + ctx.Err().Error()
	}
}

func main() {
	// ответ уже готов, таймаут в секунду не понадобится
	ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second)
	defer cancel1()
	fast := make(chan string, 1)
	fast <- "ok"
	fmt.Println(call(ctx1, fast))

	// ответа нет, таймаут 20 мс истечёт
	ctx2, cancel2 := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel2()
	slow := make(chan string) // в него никто не пишет
	fmt.Println(call(ctx2, slow))
}
```

```text
ответ: ok
не дождались: context deadline exceeded
```

Первый вызов возвращается сразу: в буфере уже лежит ответ, а `ctx1.Done()` ещё не закрыт.
Второй ждёт 20 мс, после чего контекст отменяется сам, и `ctx.Err()` возвращает
`DeadlineExceeded`.

## Обязательный cancel

Самая частая ошибка — не вызвать `cancel`. Все три функции `WithCancel`, `WithTimeout` и
`WithDeadline` возвращают `cancel`, и вызвать её нужно **всегда**. Идиома — `defer cancel()`
на следующей строке после создания:

```go
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel() // даже если doWork закончил вовремя
res, err := doWork(ctx)
```

Зачем вызывать `cancel`, если операция уже успешно закончилась? Дочерний контекст
зарегистрирован у родителя, чтобы получить от него отмену, а у `WithTimeout` ещё и
запущен таймер. Пока не вызван `cancel` (или не отменён родитель, или не наступил
дедлайн), эти ресурсы не освобождаются. Если родитель живёт долго, например это контекст
всего сервиса, а дочерние создаются на каждый запрос, неосвобождённые контексты будут
копиться.

`go vet` ловит забытый `cancel` (проверка `lostcancel`):

```text
the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak
```

Повторный вызов `cancel` безопасен и ничего не делает, так что `defer cancel()` не мешает
отменить контекст раньше вручную.

## Передача значений

`context.WithValue` прикрепляет к контексту пару «ключ — значение». Её видят все функции,
которым передали этот контекст или его потомков. Предназначение узкое: данные, которые
относятся к конкретному запросу и нужны на многих уровнях, — идентификатор запроса,
данные трассировки, сведения об аутентификации.

Ключ делают значением собственного неэкспортируемого типа. Тогда ключ из чужого пакета
не совпадёт с вашим, даже если строка внутри одинаковая:

```go play
package main

import (
	"context"
	"fmt"
)

type ctxKey string // свой тип ключа; в пакете его не экспортируют

const reqIDKey ctxKey = "reqID"

func withReqID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, reqIDKey, id)
}

func logLine(ctx context.Context, msg string) {
	id, ok := ctx.Value(reqIDKey).(string)
	if !ok {
		id = "<нет>"
	}
	fmt.Printf("[req=%s] %s\n", id, msg)
}

func main() {
	ctx := withReqID(context.Background(), "abc-123")
	logLine(ctx, "начали обработку")
	logLine(ctx, "сходили в БД")

	logLine(context.Background(), "запрос без id")
}
```

```text
[req=abc-123] начали обработку
[req=abc-123] сходили в БД
[req=<нет>] запрос без id
```

`logLine` получила идентификатор, хотя он не передавался отдельным параметром.
`ctx.Value` возвращает `any`, поэтому нужно приведение типа с проверкой `ok`: значения
может не оказаться.

Правила использования:

- Только данные запроса. Логгер, конфигурация, подключение к базе — это зависимости, их
  передают явными аргументами или полями структуры сервиса.
- Если значение влияет на бизнес-логику, ему место в сигнатуре функции. Параметр,
  спрятанный в контексте, не виден компилятору и IDE, а его отсутствие обнаружится только
  во время работы.

## Новые функции пакета: причина отмены, отвязка, колбэк

В Go 1.20 и 1.21 в пакет добавили функции, которые часто встречаются в современном коде.

`context.WithCancelCause` возвращает функцию отмены, принимающую ошибку-причину.
`ctx.Err()` по-прежнему вернёт `context.Canceled`, а `context.Cause(ctx)` — переданную
причину. Это помогает в логах отличить «клиент ушёл» от «упала соседняя задача».
Для дедлайнов есть `WithTimeoutCause` и `WithDeadlineCause`.

```go
ctx, cancel := context.WithCancelCause(parent)
defer cancel(nil)

cancel(errors.New("реплика вернула 500"))
fmt.Println(ctx.Err())          // context canceled
fmt.Println(context.Cause(ctx)) // реплика вернула 500
```

`context.WithoutCancel(ctx)` создаёт контекст, который сохраняет значения родителя, но не
отменяется вместе с ним. Пригодится, когда после ответа клиенту нужно дописать аудит-лог:
контекст запроса к этому моменту уже отменён.

`context.AfterFunc(ctx, f)` вызывает `f` в отдельной горутине после отмены `ctx`.
Возвращает функцию `stop`, которая отменяет эту регистрацию. Удобно, чтобы связать
отмену контекста с API, которое о контекстах ничего не знает, например закрыть
соединение.

## Перед задачей 13: HTTP-запрос с контекстом

HTTP-запрос обращается к серверу по адресу (URL), например `https://example.com/data`.
Метод GET запрашивает данные. Сервер отвечает кодом статуса (200 — успех, 404 — не
найдено) и телом ответа. Тело `resp.Body` — поток байтов: его читают, например,
`io.ReadAll(resp.Body)`, а после чтения обязательно закрывают через `resp.Body.Close()`,
иначе соединение не вернётся в пул и будет держать ресурсы.

Запрос с контекстом в задаче 13 собирается так:

1. создать производный контекст с таймаутом и сразу поставить `defer cancel()`;
2. создать запрос через `http.NewRequestWithContext(ctx, http.MethodGet, url, nil)` и
   проверить ошибку;
3. выполнить `http.DefaultClient.Do(req)` и проверить ошибку; если ошибка есть, ответа
   нет и обращаться к `resp` нельзя;
4. поставить `defer resp.Body.Close()`;
5. прочитать тело и проверить ошибку чтения.

Две тонкости. Таймаут ограничивает ожидание и сверху ограничен родительским
контекстом: если у родителя осталось 50 мс, ребёнок с таймаутом 200 мс отменится через
50. И ответ с кодом 404 или 500 — не ошибка `Do`: `Do` вернёт `err == nil`, а код нужно
проверять отдельно, если этого требует условие. Сети в браузерной песочнице нет, такой
код проверяйте локально.

## Graceful shutdown

Корректное завершение сервиса — место, где дерево отмены видно целиком.
`signal.NotifyContext` превращает сигналы ОС (Ctrl+C, `SIGTERM` от оркестратора) в отмену
контекста:

```go
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go runWorkers(ctx) // фоновая работа получает ctx и следит за ctx.Done()

<-ctx.Done() // ждём сигнала

shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
    // не успели за 5 секунд: например, context deadline exceeded
}
```

Несколько вещей, которые здесь легко упустить:

- Отмена корневого контекста доходит только до тех, кому этот контекст (или его потомок)
  передали и кто проверяет `Done()`.
- Контексты HTTP-запросов не становятся потомками этого корня автоматически. Связать их
  можно через поле `http.Server.BaseContext`, если это нужно.
- `Shutdown` нужен свой, ещё не отменённый контекст: `ctx` после `<-ctx.Done()` уже
  отменён, и `Shutdown` с ним вернётся немедленно, не дав обработчикам доработать.
- Сигнал «останавливайтесь» и факт «все остановились» — разные события. Дождаться
  фоновых горутин нужно отдельно, через `WaitGroup` или канал завершения.

Сигналов ОС и сети в браузере нет, поэтому пример без кнопки запуска.

## Первый успешный ответ

Задача 15 и типичный сценарий: отправить запрос нескольким репликам, взять первый
успешный ответ, остальные отменить. Контекст делает отмену проигравших одной строкой:

```go
func firstSuccess(ctx context.Context, replicas []Replica) (Result, error) {
    if len(replicas) == 0 {
        return Result{}, errors.New("нет реплик")
    }
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // при выходе отменит всех, кто ещё работает

    type reply struct {
        result Result
        err    error
    }
    results := make(chan reply, len(replicas)) // место для ответа каждой реплики
    for _, r := range replicas {
        go func(r Replica) {
            res, err := r.Query(ctx) // Query должен сам реагировать на ctx
            results <- reply{res, err}
        }(r)
    }

    var lastErr error
    for range replicas {
        select {
        case res := <-results:
            if res.err == nil {
                return res.result, nil
            }
            lastErr = res.err
        case <-ctx.Done():
            return Result{}, ctx.Err()
        }
    }
    return Result{}, lastErr // все реплики ответили ошибкой
}
```

Разберём, почему здесь нет утечек. Буфер `results` вмещает ответ каждой реплики, поэтому
горутины, которые закончат после того, как мы вернули результат, положат ответ в буфер и
завершатся, а не зависнут на отправке. `defer cancel()` сообщает проигравшим, что их
ответ больше не нужен; насколько быстро они остановятся, зависит от того, проверяет ли
`Query` контекст. Ветка `<-ctx.Done()` нужна на случай, если отменят внешний контекст.

Та же идея «первый из нескольких» уже встречалась в «или»-канале из задачи 01 (см.
[главу select](/go/book/select)); контекст здесь заменяет самодельные done-каналы.

## Частые ошибки

- **Нет `defer cancel()`.** Контекст и его таймер не освобождаются до отмены родителя.
  `go vet` об этом предупреждает.
- **Долгий цикл не проверяет `ctx.Done()`.** Отмена есть, но код её не замечает.
- **Контекст в поле структуры или `nil` вместо контекста.** Передавайте аргументом; если
  контекста пока нет — `context.TODO()`.
- **Зависимости в `WithValue`.** Логгер, конфигурация, пул соединений — явные аргументы.
- **Своя ошибка вместо `ctx.Err()`.** Вызывающему важно знать, отменили операцию
  (`Canceled`) или вышло время (`DeadlineExceeded`). Возвращайте `ctx.Err()` или
  оборачивайте его через `fmt.Errorf("...: %w", ctx.Err())`.
- **`cancel` считают ожиданием.** `cancel()` возвращается сразу. Чтобы убедиться, что
  горутины закончили, их нужно дождаться отдельно.

## Мысленная модель

Context — дерево сигналов отмены с прикреплёнными дедлайнами и значениями. Отмена идёт
сверху вниз через закрытие каналов `Done()`. Каждая функция, создающая контекст, выдаёт
`cancel`, и его нужно вызвать. Context ничего не прерывает силой: работа заканчивается
там, где код сам проверяет сигнал.

## Попробуйте сами

Создайте родительский контекст через `WithCancel` и от него два дочерних, `a` и `b`,
тоже через `WithCancel`. Отмените только `a` и запишите, что вернёт `Err()` у всех трёх.
Затем отмените родителя и снова запишите. Если вместо простых проверок контексты переданы
горутинам, как убедиться, что те действительно завершились?

### Проверка

```go
parent, cancelParent := context.WithCancel(context.Background())
defer cancelParent()
a, cancelA := context.WithCancel(parent)
defer cancelA()
b, cancelB := context.WithCancel(parent)
defer cancelB()

cancelA()
fmt.Println(parent.Err(), a.Err(), b.Err())
cancelParent()
fmt.Println(parent.Err(), a.Err(), b.Err())
```

```text
<nil> context canceled <nil>
context canceled context canceled context canceled
```

Отмена идёт от родителя вниз; братья друг друга не отменяют. `Err()` здесь можно
проверять сразу после вызова `cancel`: к моменту возврата из `cancel` контекст и все его
потомки уже отменены. Чтобы дождаться горутин, которым передали контексты, нужен
`WaitGroup` или канал завершения: `cancel` сообщает о событии, но не подтверждает, что
горутины закончили работу.

## Что дальше

Следующая глава, [«Паттерны»](/go/book/patterns), собирает из каналов, `select` и
контекста конвейеры и пулы воркеров, которые останавливаются одной отменой.

В задачах курса context — основа [блока 4](/go/topics/4) (задачи 13–15: HTTP с таймаутом, отмена набора
фоновых задач, первый успешный ответ). В [блоке 7](/go/topics/7) он есть в каждом пуле: отмена задач,
остановка с таймаутом. Как проверить, что после отмены не осталось горутин, разобрано в
главе [«Утечки горутин и гонки»](/go/book/leaks-and-races).
