Конкуренция в Go
Теория: Ошибки и конкуренция
Когда все выполняется в одном потоке, ошибок немного: функция вернула error, вызывающий ее код посмотрел и решил, что делать дальше. С горутинами такой роскоши нет. Ошибка может случиться «где-то в стороне», в отдельной горутине, и если заранее не продумать протокол, она просто потеряется в логах.
В конкурентном коде всегда нужен явный путь, по которому ошибки возвращаются к «владельцу» операции. Для этого используются каналы (chan error) и контекст (context.Context).
Передача ошибок из горутин
Базовый принцип простой: каждая горутина либо тихо завершилась успешно, либо отправила свою ошибку туда, где ее ждут.
Самый прямой способ — завести отдельный канал для ошибок и дождаться завершения всех горутин через sync.WaitGroup.
Что здесь важно:
workerвозвращает обычныйerror— это локальное решение внутри горутины.- Владелец параллельной работы создает
errsи отвечает за закрытие канала. - Все горутины пишут только в
errs, но не закрывают его — иначе будетpanic: close of closed channel. - После
wg.Wait()гарантировано, что новых записей не будет, и канал можно закрывать безопасно.
Этот паттерн подходит, когда нужно собрать все ошибки и потом решить, как с ними жить (логировать, агрегировать, считать статистику).
Иногда вместе с ошибкой нужно вернуть и результат. Тогда вместо отдельного chan error удобнее использовать один канал структур с полем Err.
Здесь данные и ошибки всегда идут вместе. Приемнику не нужно синхронизировать два канала — достаточно одного for range.
Каналы ошибок (chan error)
chan error — не отдельная магия, а все тот же канал, просто с устоявшимся назначением: собирать ошибки из конкурентных участков и передавать их в одну точку принятия решения.
Несколько типичных схем:
1. Собрать все ошибки
Уже рассмотренный пример: буферный канал, WaitGroup(), закрытие после wg.Wait(). Подходит, когда важно узнать полную картину — какие задачи упали, какие прошли.
2. «Первая ошибка — стоп»
Иногда продолжать работу после первого сбоя бессмысленно. В этом случае канал ошибок обычно буферизуют размером 1 и читают только первую ошибку. Остальные пытаются записаться, но не блокируют систему:
Здесь chan error играет роль однокомпонентного «почтового ящика»: кто первым донес плохую новость, тот и запустил отмену.
3. Правила работы с каналом ошибок
- Канал ошибок закрывает только читатель, когда уверен, что все отправители завершились (после
wg.Wait()). - Если канал не буферизовать и не читать из него параллельно, горутины с
errs <- errзависнут. Поэтому либо буфер, либо отдельная горутина-сборщик. - Запись в закрытый канал мгновенно падает в
panic, поэтому закрытие всегда должно быть централизованным.
Использование контекста для ошибок
Канал ошибок отвечает на вопрос «что случилось», но сам по себе не дает сигнал остальным горутинам, что пора остановиться. Для этого нужен context.Context.
Связка выглядит так:
- каждая горутина принимает
ctxи периодически проверяет<-ctx.Done(), - при первой значимой ошибке вызывается
cancel(), - все остальные горутины видят отмену контекста и корректно завершаются.
Что здесь делает контекст:
- связывает все горутины в одну «операцию»;
- дает единый канал
Done(), который срабатывает и при явномcancel(), и при таймауте/дедлайне; - не прерывает горутину грубо, а дает сигнал, который она сама должна обработать.
В результате получается четкий протокол:
- Любая горутина может обнаружить ошибку.
- Ошибка передается через
chan errorк владельцу операции. - Владелец (или та же горутина) вызывает
cancel(). - Все остальные горутины получают
<-ctx.Done()и аккуратно останавливаются. - После
wg.Wait()канал ошибок закрывается, и система завершает работу в контролируемом состоянии.
Ошибки в конкурентном коде — не побочный эффект, а часть протокола. Если с самого начала заложить в архитектуру chan error и context.Context, программа будет не только параллельной, но и управляемой: ошибки видно, реакции на них предсказуемы, а горутины не живут дольше, чем нужно.


