Конкуренция в Go
Теория: Контекст выполнения (context)
Контекст в Go связывает между собой горутины, которые участвуют в одной общей операции. Он передает по цепочке сигналы отмены, дедлайны и таймауты, а также может нести дополнительные значения. Контекст сам ничего не «убивает» и не прерывает. Он подает сигнал, что продолжать работу больше не нужно, а каждая горутина принимает решение завершиться самостоятельно. Так формируется управляемый жизненный цикл конкурентного кода.
Сигналы отмены и дедлайны
Контекст особенно полезен там, где одна операция порождает несколько горутин. Например, HTTP-запрос запускает обработку, вызывает внешние сервисы, пишет в лог, обновляет кеш. Все эти действия составляют одну логическую задачу. Если запрос прервался или истек таймаут, продолжать работу не имеет смысла. Контекст позволяет передать этот факт по всей цепочке.
Основой служит канал Done(), который принадлежит каждому контексту. Пока контекст активен, чтение из этого канала блокируется. Когда вызывается отмена или наступает дедлайн, канал закрывается. Все горутины, которые слушают <-ctx.Done(), получают сигнал и выходят из своих циклов.
Здесь одна точка отмены управляет сразу тремя воркерами. Как только вызывается cancel(), канал Done() для контекста закрывается, и каждая горутина завершает работу при следующей итерации select. Ошибка ctx.Err() показывает причину: при ручной отмене это context.Canceled, при дедлайне — context.DeadlineExceeded.
То же поведение можно привязать ко времени. В этом случае WithTimeout создает контекст, который автоматически отменяется после заданной длительности, а WithDeadline — в конкретный момент времени.
Здесь контекст подсказывает, что предел ожидания достигнут, и связанным горутинам пора остановиться. Вместо самодельных таймеров и флагов используется единый сигнальный механизм.
context.Background() и context.TODO()
Любое дерево контекстов начинается с корневого узла. В Go для этого есть два исходных варианта: context.Background() и context.TODO(). Оба создают пустой контекст без дедлайна, таймаута и значений. Разница между ними в том, какое намерение показывает код.
Background выступает как корень системы. Его обычно используют в main(), при инициализации сервера, запуске фоновых задач, от которых зависят остальные части программы. Такой контекст живет столько же, сколько живет приложение, и из него строятся все дочерние контексты.
TODO используют как заглушку. Это явный сигнал, что контекст здесь нужен, но источник еще не определен. Функция уже принимает context.Context, но вызывающая сторона пока не может передать реальный контекст запроса, соединения или задачи.
Пока TODO и Background ведут себя одинаково, но по коду видно, где начинается реальное дерево контекстов, а где его еще предстоит протянуть от внешнего источника.
WithCancel, WithDeadline, WithTimeout
Поведение контекста не меняется напрямую. Вместо этого поверх родительского контекста создается новый — с дополнительными правилами жизни. На этом построены функции WithCancel, WithDeadline и WithTimeout. Каждая из них возвращает пару: новый контекст и функцию отмены, которая освобождает ресурсы и подает сигнал вниз по дереву.
WithCancel добавляет ручную отмену.
WithDeadline задает жесткий момент во времени, после которого контекст считается просроченным.
WithTimeout упрощает задачу, когда нужен не конкретный момент, а длительность. Внутри он просто вызывает WithDeadline с time.Now().Add(d).
Все дочерние контексты наследуют состояние родителя. Если родитель отменен, все потомки получают сигнал Done() и завершают работу. Именно поэтому важно вызывать cancel() всегда, когда контекст создается через WithCancel, WithDeadline или WithTimeout, даже если операция уже завершилась успешно. Это освобождает внутренние таймеры и предотвращает утечки ресурсов.
Завершение горутин по сигналу
Контекст связывает запуск и остановку горутин в единую линию. Любая долгоживущая горутина должна иметь точку выхода, завязанную на внешний сигнал. Тогда прекращение работы запроса, сервиса или приложения автоматически тянет за собой завершение всех связанных задач.
В этом примере каждая горутина регулярно проверяет ctx.Done() внутри своего цикла. Как только контекст отменяется, worker завершает работу сам, не оставляя «висящих» операций.
Иногда вместо контекста используют сигнальный канал. Принцип остается тем же: горутина слушает внешнее событие и выходит, когда канал закрывается.
Для взаимодействия с операционной системой используется связка контекста и os/signal. В этом случае завершение приложения по Ctrl+C превращается в тот же самый сигнал Done(), который получают все связанные горутины.
Здесь нажатие Ctrl+C приводит к отмене контекста, закрытию Done() и последовательному завершению всех связанных горутин.
Контекст выстраивает управление конкурентным кодом в понятную схему. Сигналы отмены и дедлайны передаются по дереву, горутины слушают Done() и корректно завершаются. Вместо разрозненных флагов, глобальных переменных и самодельных каналов появляется единый механизм, который описывает: когда задача начинается, когда должна закончиться и что происходит, если время истекло раньше результата.


