Коротко: context.Context передаёт через цепочку вызовов отмену, таймаут и значения запроса — вовремя останавливает ненужную работу и спасает от утечек горутин.

Коротко: context.Context — это способ передать через цепочку вызовов сигнал отмены, дедлайн/таймаут и небольшие значения, привязанные к запросу. Главная задача — вовремя останавливать работу (запросы к БД, HTTP-вызовы, горутины), когда результат уже не нужен. Это спасает от утечек горутин и «висящих» операций. Разберём, как и где его применять.

Зачем вообще нужен context

Представьте HTTP-запрос, который внутри ходит в базу и во внешний сервис. Если клиент отключился или истёк таймаут — продолжать эту работу бессмысленно, она только жжёт ресурсы. context передаёт сигнал «всё, отменяемся» сверху вниз по всем вызовам.

Правило передачи

Контекст принято передавать первым аргументом функции с именем ctx и никогда не хранить в структуре:

func fetchUser(ctx context.Context, id int) (*User, error) {
    // ctx прокидывается дальше во все вызовы
    return repo.Get(ctx, id)
}

Корневые контексты

В начале цепочки берут «пустой» контекст:

ctx := context.Background() // корневой, в main / на старте
ctx := context.TODO()       // заглушка, когда ещё не решили, какой context

В HTTP-обработчике контекст уже есть в запросе: r.Context() — он отменяется автоматически, когда клиент разрывает соединение.

Таймаут и дедлайн

Чаще всего context используют, чтобы ограничить время операции:

ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // ОБЯЗАТЕЛЬНО, иначе утечка таймера

result, err := slowCall(ctx)
if err != nil {
    // при таймауте err будет context.DeadlineExceeded
}

defer cancel() нужен всегда — он освобождает ресурсы таймера, даже если операция успела завершиться раньше.

Явная отмена

WithCancel даёт функцию, которой можно отменить контекст вручную — например, остановить пачку горутин:

ctx, cancel := context.WithCancel(context.Background())

go worker(ctx)
go worker(ctx)

// где-то по условию:
cancel() // обе горутины получат сигнал через ctx.Done()

Как реагировать на отмену

Горутина или цикл должны слушать ctx.Done() и выходить, когда канал закрылся:

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("останавливаюсь:", ctx.Err())
            return
        default:
            doChunkOfWork()
        }
    }
}

ctx.Err() подскажет причину: context.Canceled (отменили вручную) или context.DeadlineExceeded (истёк таймаут).

Значения в контексте — осторожно

context.WithValue позволяет прокинуть данные запроса (request ID, пользователь), но это не замена параметрам функций:

type ctxKey string
const requestIDKey ctxKey = "requestID"

ctx = context.WithValue(ctx, requestIDKey, "abc-123")

// далее
id, _ := ctx.Value(requestIDKey).(string)

Правило: в context кладут только сквозные «request-scoped» метаданные (трассировка, авторизация), а не бизнес-аргументы. И ключ делают своим типом (не строкой напрямую), чтобы избежать коллизий.

Типичные ошибки

  • Забыли defer cancel() — утечка ресурсов таймера.
  • Хранят ctx в поле структуры — антипаттерн; передавайте аргументом.
  • Не прокидывают ctx в нижележащие вызовы (БД, HTTP) — тогда отмена не работает.
  • Кладут в context бизнес-данные вместо параметров — код становится непрозрачным.
  • Передают nil вместо контекста — используйте context.TODO().

Грамотная работа с context — то, что отличает надёжный Go-сервис от того, что течёт горутинами и висит на таймаутах под нагрузкой. Если проектируете сервис с внешними вызовами и хотите заложить отмену и таймауты правильно — это тема для консультации.