Конкуренция в Go
Теория: Синхронизация с каналами
Когда каналы уже освоены как средство передачи данных, открывается их второе назначение. Канал в Go — это одновременно очередь значений, средство коммуникации горутин и механизм их синхронизации.
Каждая операция отправки или приема — это точка встречи: две независимые горутины выравниваются во времени именно в этот момент. На этом свойстве строится синхронизация — вместо флагов и мьютексов программа просто ждет нужное сообщение или событие.
Каналы как средство синхронизации
Иногда сами данные не важны. Значение может выступать лишь маркером события: «задача началась», «задача завершилась», «нужно остановиться». В таких случаях канал превращается в сигнальный. Одна горутина отправляет в него факт, другая ждет этот факт и продолжает работу только после его получения.
Здесь используется chan struct{}, потому что значимость имеет только сам факт отправки. Пустая структура не занимает памяти, а тип канала четко показывает, что через него не передаются данные, а проходят сигналы. Основная горутина блокируется на чтении из done, пока worker не отправит сообщение о завершении.
Ожидание завершения группы горутин
Когда параллельных задач несколько, синхронизация превращается в ожидание не одного, а целого набора сигналов. Самый простой путь — имитировать поведение sync.WaitGroup через общий канал: каждая горутина отправляет уведомление, а основная горутина столько же раз читает из канала.
Канал действительно работает как точка синхронизации: каждая горутина в своем темпе доходит до отправки сигнала, а основная по очереди их получает. Формально поведение корректное.
Но в реальных программах такой паттерн почти всегда считается неудачным. Он хрупкий: количество ожидаемых сигналов нужно знать заранее, и любое изменение числа горутин ломает протокол. Если задач становится больше или меньше, цикл чтения придется править вручную. Кроме того, канал тут не несет полезных данных — он используется как костыль для ожидания завершения.
Для таких сценариев в Go уже есть специально предназначенный инструмент — sync.WaitGroup. Он явно задает количество работ и гарантирует корректное ожидание, не требуя ручного подсчета сигналов:
Такой код проще, надежнее и масштабируется лучше. Каналы уместны там, где есть поток данных или протокол обмена сообщениям, а ожидание завершения группы горутин — задача именно для WaitGroup(), а не для сигналов через канал.
Пул воркеров на каналах
Каналы особенно ясно проявляют себя в шаблоне worker pool. Одна часть программы формирует задачи и складывает их в канал jobs. Несколько горутин воркеров читают из этого канала, обрабатывают данные и отправляют результаты в другой канал. Закрытие jobs служит сигналом, что новых задач не будет, и воркеры могут завершать цикл обработки.
В этой схеме каналы выполняют сразу несколько функций. jobs распределяет работу между воркерами, results собирает результаты, а закрытие jobs завершает внутренний цикл for range во всех рабочих горутинах. Порядок получения результатов не гарантируется, но гарантируется отсутствие взаимных блокировок: каждый воркер завершит работу, как только задач в jobs не останется.
Fan-out и fan-in на каналах
Fan-out / fan-in — это про форму потока данных: одна линия распадается на несколько, потом несколько сходятся обратно в одну.
Worker pool — это частный случай такого fan-out: у нас есть очередь задач и несколько одинаковых воркеров, которые ее разгребают. Разница больше в акценте: fan-out / fan-in описывает движение данных, worker pool — организацию «бригад» обработчиков и ограничение конкуренции.
По коду они выглядят очень похоже, так что в примерах важно проговорить, что мы показываем именно форму потока, а не “идеальный пул”.
Fan-out: один поток задач, несколько воркеров
Оставим твой пример, но чуть подчистим комментарии, чтобы было видно, что это и fan-out, и фактически worker pool:
Здесь входной поток чисел расширяется на три воркера — это и есть fan-out. По сути, тот же worker pool, просто без ограничителя «максимум N задач одновременно» и без отдельной логики остановки воркеров, кроме закрытия канала.
Fan-in: несколько источников, один сборщик (без panic)
Каналы в Go удобно использовать не только как очередь значений. Они помогают строить параллельные схемы, где поток данных расширяется на несколько обработчиков или, наоборот, собирается обратно в одну линию. Эти две формы называются fan-out и fan-in.
Канал остается средством передачи данных и одновременно точкой синхронизации: операции отправки и чтения выстраивают горутины во времени и задают порядок работы.
Fan-out: один поток разделяется на несколько воркеров
В fan-out одна горутина отправляет значения в канал, а несколько рабочих горутин обрабатывают их параллельно. Это похоже на worker pool, но цель здесь — показать именно расширение потока данных.
Закрытие in завершает работу всех воркеров. Каждый из них дочитывает канал до конца и завершает свою горутину без блокировок.
Fan-in: несколько источников объединяются в один поток
Fan-in — обратная схема: несколько горутин отправляют данные в один общий канал, а одна горутина последовательно читает все сообщения.
Главное отличие от предыдущего примера — нельзя закрывать канал вслепую. Закрывать его должен тот, кто точно знает, что все отправители закончили работу. Поэтому используется WaitGroup().
Такой вариант безопасен: канал закрывается только после того, как все отправители отработали.
Чем fan-out отличается от worker pool
Разница в акценте:
- fan-out — про форму потока: одна линия превращается в несколько параллельных;
- worker pool — про управление нагрузкой и числом активных обработчиков.
В простых примерах они выглядят одинаково, но в fan-out важен именно поток данных, а не ограничение ресурсов.
Fan-out и fan-in — это базовые схемы построения параллельных конвейеров. Закрытие входного канала корректно завершает работу всех воркеров, а закрытие выходного канала через WaitGroup() позволяет безопасно получить объединенный поток сообщений от нескольких источников.
Подведем итоги
Синхронизация через каналы строится не вокруг флагов, счетчиков и ручных блокировок, а вокруг движения данных и сигналов. Канал становится местом встречи горутин, а операция <- — естественной точкой ожидания. Через сигнальные каналы оформляется завершение задач, через каналы задач и результатов — работа пулов воркеров, через общие каналы — схемы fan-out и fan-in. В результате координация превращается в управление потоками значений, а порядок действий задается протоколом взаимодействия, а не набором низкоуровневых примитивов.


