Таймаут в десяти строках
Задача context.Context в том, чтобы сообщить коду, когда остановиться. Здесь медленная операция получает 50 мс и сдаётся, когда об этом говорит контекст:
Первый вызов завершается за 10 мс и возвращает rows <nil>. Второму нужно 200 мс, но контекст истекает на 50 мс (считая от момента создания), поэтому он возвращает context deadline exceeded.
Ничто не останавливается принудительно. В Go нет способа убить горутину снаружи. Контекст это сигнал, и код должен его проверять: делать select по ctx.Done(), проверять ctx.Err() между шагами или передавать ctx в библиотечные вызовы (http.NewRequestWithContext, db.QueryContext, exec.CommandContext), которые проверяют его за вас.
Интерфейс Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
| Метод | Возвращает |
|---|---|
Done() | канал, который закрывается, когда контекст отменён или истёк (nil для контекста, который отменить нельзя) |
Err() | nil, пока контекст активен, затем context.Canceled или context.DeadlineExceeded |
Deadline() | дедлайн и true либо ok == false, если дедлайна нет |
Value(key) | значение, сохранённое под key в этом контексте или предке, либо nil |
Контексты неизменяемы. Контекст никогда не меняют; от него порождают дочерний с помощью одной из функций With, и дочерний добавляет сигнал отмены, дедлайн или значение.
Откуда берётся контекст
Каждое дерево контекстов начинается с корня:
context.Background()дляmain,init, тестов и настройки серверов верхнего уровня.context.TODO(), когда функция должна принимать контекст, но у вызывающего кода его ещё нет. Ведёт себя точно какBackground; имя служит пометкой для будущего рефакторинга.
Внутри HTTP-обработчика корень не создают. Используют r.Context(), который сервер отменяет, когда клиент отключается или обработчик возвращает управление.
WithCancel: остановка по требованию
context.WithCancel возвращает дочерний контекст и функцию cancel. Вызов cancel закрывает канал Done дочернего контекста и каналы Done всего, что от него порождено.
Отправка производителя стоит в select рядом с ctx.Done(). Именно это позволяет ему остановиться: голое out <- i заблокировалось бы навсегда, как только потребитель перестал читать, и горутина утекла бы. Последний for range nums ждёт, пока производитель закроет канал. Пока он вычитывает канал, производитель ещё может успеть отправить одно-два значения, потому что когда обе ветки его select готовы, Go выбирает одну случайно; отмена срабатывает быстро, но не мгновенно.
cancel можно безопасно вызывать больше одного раза и из любой горутины. Что-то делает только первый вызов.
WithTimeout и WithDeadline
WithTimeout(parent, d) это WithDeadline(parent, time.Now().Add(d)). Таймаут подходит для «не дольше чем», а дедлайн, когда есть абсолютное время.
Когда время прошло, Done закрывается, а Err возвращает context.DeadlineExceeded. Если раньше вызвали cancel, Err возвращает context.Canceled. Какой из вариантов, проверяйте через errors.Is, потому что библиотеки обычно оборачивают ошибку:
Всегда вызывайте cancel, даже для таймаута, который сработает сам. Пока не случится одно из этого, контекст держит таймер и место в родителе, а defer cancel() освобождает и то, и другое, как только функция вернётся. go vet сообщает о выброшенной функции отмены: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.
Дочерние контексты не переживают родителей
Контексты образуют дерево. Отмена родителя отменяет всех потомков. У дочернего контекста дедлайн может быть короче, чем у родителя, но никогда не длиннее: побеждает всегда более ранний дедлайн.
Именно это делает контексты полезными между слоями. HTTP-обработчик получает контекст, который умирает вместе с запросом; вызов базы данных на три слоя ниже порождает из него 2-секундный таймаут. Если клиент отключится через 100 мс, запрос отменится тогда же, а не через две секунды.
Всегда делайте select по ctx.Done(), когда блокируетесь
Любая горутина, которая ждёт (отправки в канал, получения, таймера), должна одновременно ждать и ctx.Done(). Для циклов, нагружающих процессор и никогда не блокирующихся, время от времени проверяйте ctx.Err():
for i, item := range items {
if i%1000 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
process(item)
}
Для простого ожидания используйте time.After внутри select, но если ожидание может часто отменяться, лучше таймер, который можно остановить (или таймаут контекста).
Причины отмены (Go 1.20 и 1.21)
ctx.Err() говорит только canceled или deadline exceeded. Чтобы записать причину, используйте варианты с Cause:
WithCancelCause появился в Go 1.20, WithTimeoutCause и WithDeadlineCause в Go 1.21. Err по-прежнему возвращает стандартные значения, так что существующие проверки продолжают работать; подробность даёт context.Cause.
WithValue, но экономно
context.WithValue(parent, key, value) прикрепляет одно значение. ctx.Value(key) ищет его по цепочке родителей.
Правила для значений:
- Используйте для ключей неэкспортированный тип, никогда не обычную
string. Два пакета, которые оба используют"user", перезапишут друг друга. (go vetэтого не ловит, аstaticcheckловит.) - Оборачивайте доступ в типизированные функции-помощники вроде
WithRequestIDиRequestID, чтобы вызывающий код никогда не видел ниany, ни ключ. - Храните только данные уровня запроса, которые проходят через API: trace ID и ID запросов, аутентифицированного пользователя, логгер. Никогда не храните необязательные параметры, дескрипторы баз данных или конфигурацию. Им место в аргументах функций или полях структур, где их проверит компилятор и увидят читатели.
- Поиск идёт по цепочке по одному родителю, поэтому каждое добавленное значение удлиняет поиск остальных на шаг.
Соглашения
ctx context.Contextэто первый параметр любой функции, которая выполняет ввод-вывод, блокируется или вызывает что-то подобное:func Fetch(ctx context.Context, url string) error.- Не храните контекст в структуре. Передавайте его в каждый вызов метода. Контекст принадлежит одной операции, а структура обычно живёт дольше. (Исключение: тип, представляющий одну операцию, например
http.Request.) - Никогда не передавайте
nilв качестве контекста. Если ничего лучше нет, используйтеcontext.TODO(). - Возвращайте
ctx.Err()или оборачивайте его через%w, когда останавливаетесь из-за контекста, чтобы вызывающий код мог отличить таймаут от настоящего сбоя.
Контекст в HTTP-серверах и клиентах
Серверная сторона: r.Context() отменяется, когда клиент отключается, когда обработчик возвращает управление или когда сбрасывается поток HTTP/2. Клиентская сторона: http.NewRequestWithContext заставляет запрос учитывать таймаут или отмену. Эта программа прогоняет обе стороны через httptest:
Клиент сдаётся на 50 мс и закрывает соединение. Сервер это замечает, контекст его запроса отменяется, и обработчик останавливается, а не тратит ещё 450 миллисекунд на отчёт, который никто не прочитает. В настоящем обработчике вы передаёте r.Context() вниз в каждый вызов базы данных и HTTP, и все они останавливаются вместе.
Другие помощники (Go 1.21)
context.WithoutCancel(ctx)возвращает контекст с теми же значениями, который не отменяется вместе сctx. Используйте его для работы, которая должна завершиться после окончания запроса, например записи журнала аудита.context.AfterFunc(ctx, f)запускаетfв отдельной горутине, как толькоctxзавершён, и возвращает функциюstop, чтобы снять регистрацию.
Частые ошибки
- Не вызван
cancel. Всегдаdefer cancel()сразу послеWithCancel,WithTimeoutилиWithDeadline. - Горутина, которая игнорирует
ctx. Если она блокируется без select поctx.Done(), отмена ничего не даёт, и горутина утекает. - Новый
context.Background()глубоко внутри цепочки вызовов. Он обрывает связь с дедлайном и отменой вызывающего кода. Передавайте тотctx, который получили. - Сравнение ошибок через
==. Используйтеerrors.Is(err, context.DeadlineExceeded); большинство библиотек её оборачивают. WithValueдля зависимостей. Дескриптор базы данных, спрятанный в контексте, это параметр, который компилятор уже не может проверить.- Ожидание мгновенной отмены. Код замечает её только при следующей проверке. Длинный цикл без проверок продолжает работать.
Часто задаваемые вопросы
Для чего нужен context в Go?
context.Context сообщает функции и всему, что она вызывает, когда пора сдаться: потому что вызывающий код отменил операцию, потому что прошёл дедлайн или потому что клиент отключился. Он может также нести значения уровня запроса, например ID запроса. По соглашению это первый параметр с именем ctx.
Чем context.Background отличается от context.TODO?
Оба возвращают пустой контекст, который никогда не отменяется и не имеет ни дедлайна, ни значений. Ведут они себя одинаково. Background() это корень для main, тестов и настройки верхнего уровня. TODO() отмечает место, куда должен передаваться настоящий контекст, но у окружающего кода его пока нет, и благодаря этому такое место легко найти потом.
Зачем вызывать cancel после context.WithTimeout?
WithTimeout, WithDeadline и WithCancel регистрируют новый контекст у родителя и могут запустить таймер. Вызов cancel освобождает эти ресурсы сразу, как только вы закончили, а не когда сработает таймаут или отменится родитель. Пишите defer cancel() сразу после создания; go vet предупреждает, если функция отмены выброшена.
Что означает «context deadline exceeded» в Go?
Это текст context.DeadlineExceeded, ошибки, которую возвращает ctx.Err(), когда дедлайн контекста прошёл. Функции, которые учитывают контекст, например HTTP-клиенты и драйверы баз данных, возвращают её (часто обёрнутой), когда у них заканчивается время. Проверяйте её через errors.Is(err, context.DeadlineExceeded).
Стоит ли передавать параметры через context.WithValue?
Нет. Используйте его только для данных уровня запроса, которые пересекают границы API и о которых промежуточным функциям знать не нужно, например trace ID или аутентифицированный пользователь. Всё, что функции нужно для работы, должно быть в её параметрах, где это проверит компилятор.