Как использовать счетчик и таймер для определения завершения всех горутин и закрытия канала в Go
Что проверяют
Умеете ли вы закрыть канал ровно один раз и только после того, как все писатели закончили, и не зависнуть навсегда, если какая-то горутина не вернулась. Следом обычно спрашивают, что будет, если вызвать wg.Add внутри горутины, и как ограничить ожидание по времени.
Как отвечать
Базовая схема из трёх частей:
- счётчик:
sync.WaitGroup.wg.Add(1)вызывается доgo, в самой горутинеdefer wg.Done(). С Go 1.25 естьwg.Go(func() { ... }), который делает то же самое и не даёт перепутать порядок; - закрытие канала: отдельная горутина делает
wg.Wait()и затемclose(results). Закрывает канал тот, кто точно знает, что писателей больше нет, то есть не воркер и не читатель; - чтение: цикл
for r := range resultsв основной горутине завершится сам, когда канал закроют.
Таймер добавляется через select. Ожидание оборачивают в канал done, который закрывается после wg.Wait(), и ждут либо его, либо time.After(5 * time.Second). Правильнее передать context.WithTimeout в сами воркеры: по ctx.Done() они бросают работу, а отправку в канал тоже делают через select, иначе воркер навсегда застрянет на записи, которую никто не прочитает.
Если воркеры возвращают ошибки, упомяните errgroup.Group из golang.org/x/sync/errgroup: g.Wait() вернёт первую ошибку, errgroup.WithContext отменит общий контекст, а g.SetLimit(n) ограничит число одновременных горутин.
Где спотыкаются
Вызывают wg.Add(1) внутри горутины. Тогда wg.Wait() может пройти раньше, чем счётчик вырос, и канал закроется, пока в него ещё пишут. Итог: panic: send on closed channel.
Закрывают канал в воркере или делают wg.Wait() в основной горутине до чтения. Первое даёт панику при повторном close, второе дедлок на небуферизованном канале: воркеры ждут читателя, а читатель ждёт воркеров.
Считают, что таймаут что-то остановил. time.After лишь прекращает ожидание, горутины продолжают работать и держат память, пока их не отменят через контекст. И порядок сообщений в канале не гарантирован, рассчитывать на вывод «по номерам» нельзя.