Что произойдёт, если передать sync.WaitGroup в функцию-воркер по значению вместо указателя?
Воркер будет вызывать Done у своей копии sync.WaitGroup, а исходный объект останется с ненулевым счётчиком. Поэтому Wait исходного объекта может заблокироваться навсегда. Кроме того, копирование WaitGroup после начала использования запрещено и может привести к некорректной синхронизации.
sync.WaitGroup предназначен для координации группы горутин через общий счётчик: одна сторона увеличивает его, горутины уменьшают, а ожидающая сторона блокируется до нулевого значения. Для такой схемы все участники должны работать с одним экземпляром состояния, а не с независимыми копиями.
Передача структуры по значению создаёт копию на границе вызова. Если главный поток вызвал Add, а затем передал WaitGroup воркеру по значению, воркер уменьшит счётчик только в копии.
Исходный счётчик останется положительным, поэтому вызов Wait не завершится. В реальной программе это проявляется как зависание, утечка горутины или остановка сервиса при ожидании завершения фоновых задач.
В WaitGroup нужно передавать указатель и не копировать объект после первого использования. Минимальный пример ошибочного и правильного вариантов:
В правильном варианте Add, Done и Wait обращаются к одному счётчику. Передача указателя также соответствует ограничению WaitGroup: его нельзя копировать после начала использования.
Передавать WaitGroup по значению нельзя даже тогда, когда кажется, что копия нужна только для удобства. Копия содержит внутреннее состояние синхронизации, и корректность её независимого использования не гарантируется. Инструмент go vet обычно обнаруживает такие копирования благодаря специальному маркеру, но полагаться только на диагностику нельзя.
WaitGroup решает именно задачу ожидания завершения, а не передачу результатов, отмену или ограничение параллелизма. Для результатов лучше использовать каналы, для отмены — контекст, а для ограничения числа одновременно работающих задач — отдельный механизм, например семафор на базе буферизованного канала.
Сервис запускает несколько обработчиков и передаёт им WaitGroup. Первый вариант — передавать структуру по значению. Он прост по синтаксису, но создаёт копии счётчика и приводит к зависанию при ожидании.
Второй вариант — передавать указатель каждому обработчику. Он сохраняет единое состояние и требует дисциплины: Add выполняется до запуска горутины, а Done обычно помещается в defer сразу после входа в обработчик.
Третий вариант — вместо WaitGroup отправлять сообщения о завершении через канал. Это удобно, если одновременно нужно передать статус или ошибку, но для одного лишь ожидания добавляет лишний протокол. Поэтому для данной задачи выбирают указатель на WaitGroup, а результаты и ошибки передают отдельно.
sync.WaitGroup до его первого использования?Нулевое значение WaitGroup можно скопировать до начала использования, поскольку счётчик и внутреннее состояние ещё не задействованы. Однако безопасный API обычно сразу принимает *sync.WaitGroup, чтобы не создавать хрупкое правило, зависящее от момента копирования.
После первого вызова Add, Done или Wait копировать объект нельзя. Даже если конкретный тест не зависает, программа нарушает контракт примитива и может работать непредсказуемо.
Done больше раз, чем был увеличен счётчик?Счётчик станет отрицательным, и WaitGroup завершит выполнение с паникой. Это обычно означает ошибку управления жизненным циклом горутины: например, Done вызван дважды или Add пропущен.
Безопасный шаблон — после входа в горутину сразу зарегистрировать defer wg.Done(). Он не устраняет логические ошибки полностью, но снижает риск забыть уменьшить счётчик на одном из путей выхода.
WaitGroup для нескольких последовательных групп горутин?Да, после того как предыдущий вызов Wait завершился и счётчик равен нулю, тот же объект можно использовать для следующей группы. Нельзя начинать новый цикл работы, пока предыдущие операции с ним ещё выполняются, и нельзя допускать конкурентное копирование.
Повторное использование не означает, что разрешено добавлять задачи без ограничений во время ожидания. Положительные вызовы Add, переводящие счётчик с нуля, должны быть выполнены до соответствующего Wait; иначе новая работа может быть не учтена ожидающим кодом.