Как устроены контексты в Go
Контексты в Go (context.Context) — это интерфейс для передачи метаданных, управления сроками выполнения и отменой операций в иерархии вызовов функций. Основная цель — дать возможность остановить выполнение программы (например, запросов или подпроцессов) по требованию, что критично в сетевых приложениях, где операции могут занимать слишком много времени или становиться ненужными.
Как это работает
Контекст — это интерфейс с четырьмя методами: Deadline(), Done(), Err() и Value(). Ключевые особенности:
- Пропагация по горутинам: контекст безопасно передается между горутинами, позволяя управлять операциями, выполняемыми параллельно.
- Отмена: контекст можно отменить, что автоматически сигнализирует всем получателям через закрытие канала
Done(). Получатели должны проверятьDone()и завершать работу. - Сроки выполнения: можно установить таймаут или дедлайн, после которого контекст отменяется автоматически.
- Значения: контекст может хранить пары ключ-значение для передачи данных, специфичных для запроса (например, ID сессии или токены авторизации).
Создание контекстов
Пакет context предоставляет несколько функций для создания контекстов:
context.Background()— пустой контекст, который никогда не отменяется. Используется вmain()и при инициализации.context.TODO()— заглушка для мест, где контекст должен быть, но еще не передан. Применяется при разработке и рефакторинге.context.WithCancel(parent)— создает дочерний контекст с функцией отменыcancel.context.WithDeadline(parent, deadline)— создает контекст с конкретным временем отмены.context.WithTimeout(parent, timeout)— создает контекст, который отменится через заданный промежуток времени.
Все производные контексты создаются от родительского и наследуют его свойства. При отмене родительского контекста все дочерние также отменяются.
Пример
package main
import (
"context"
"fmt"
"time"
)
func operation1(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("operation1 completed")
case <-ctx.Done():
fmt.Println("operation1 cancelled")
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // важно вызывать cancel, чтобы освободить ресурсы
go operation1(ctx)
// Дожидаемся завершения или отмены операции
<-ctx.Done()
if err := ctx.Err(); err != nil {
fmt.Println("main:", err)
}
}В этом примере operation1 прерывается, если контекст отменяется до истечения 5 секунд. В main мы ждем отмены контекста и выводим причину (context.DeadlineExceeded).
Подводные камни
- Всегда вызывайте
cancel: если не вызвать функцию отмены, ресурсы (таймеры, горутины) будут утекать. Используйтеdefer cancel()сразу после создания. - Не храните контекст в структурах: контекст должен передаваться явно как первый аргумент функции. Хранение в структурах затрудняет тестирование и нарушает идиоматичность.
- Не передавайте
nilконтекст: вместо этого используйтеcontext.TODO()илиcontext.Background(). - Значения контекста не должны быть критичными: они предназначены для запрос-специфичных данных, а не для передачи параметров функций.
- Проверяйте
ctx.Err(): после отмены контекстаErr()возвращаетcontext.Canceledилиcontext.DeadlineExceeded, что помогает понять причину.
Когда использовать
Контексты обязательны в HTTP-обработчиках, при работе с базами данных, внешними API и любыми длительными операциями. Они позволяют корректно завершать работу при отмене запроса или превышении таймаута, экономя ресурсы и улучшая отзывчивость приложения.
Коротко
- Контекст — это интерфейс для отмены, таймаутов и передачи метаданных.
- Создавайте контексты через
context.Background(),WithCancel,WithTimeout,WithDeadline. - Всегда вызывайте
cancelчерезdefer, чтобы избежать утечек. - Проверяйте
ctx.Done()в длительных операциях и завершайте работу при отмене.