Разберите последствия: программа запускает горутину перед возвратом из main; гарантировано ли, что горутина успеет завершить работу?
Нет. Когда функция main завершается, процесс Go заканчивает работу, а остальные горутины не дожидаются завершения и могут быть прерваны в любой момент. Для гарантии результата main должен явно дождаться горутины, например через WaitGroup, канал результата или другой механизм координации.
Модель конкурентности Go предполагает, что горутины — лёгкие единицы выполнения внутри одного процесса, а не независимые фоновые задачи с собственным жизненным циклом. Поэтому запуск горутины не меняет правило завершения программы: окончание main означает окончание процесса.
Такой подход избавляет runtime от необходимости самостоятельно определять, какие фоновые задачи нужно ждать. Ответственность за жизненный цикл работы остаётся у программы: она должна явно описать, когда все необходимые операции завершены.
Типичная ошибка возникает, когда main запускает горутину для записи файла, отправки сообщения или выполнения вычисления, а затем сразу возвращает управление. Иногда горутина успевает выполниться, особенно на загруженной системе, но это случайный результат, а не гарантия.
При завершении процесса незавершённая горутина может не записать данные, не вызвать свои defer, не освободить внешние ресурсы и не сообщить об ошибке. Добавление Sleep лишь маскирует проблему: нужная задержка неизвестна, а выполнение всё равно остаётся недетерминированным.
main запускается как обычная горутина, но её возврат имеет специальное значение: runtime завершает программу. Другие горутины не превращаются в независимые процессы и не продолжают работу после завершения процесса.
Для явного ожидания можно использовать sync.WaitGroup:
Add должен быть выполнен до запуска горутины, а каждая запущенная горутина обязана вызвать Done, обычно через defer. WaitGroup сообщает только о завершении, но не передаёт результат и не отменяет работу.
Если нужно получить значение или ошибку, лучше применить канал результата либо специализированную группу задач с распространением ошибок и отменой. В серверном приложении завершение обычно координируют через context.Context: новые операции перестают запускаться, текущие получают сигнал отмены, а управляющая горутина ждёт их завершения.
CLI-утилита после обработки входных данных запускает горутину для записи отчёта и сразу выходит. Вариант с time.Sleep прост, но ненадёжен: на медленном диске отчёт может не записаться, а на быстром программа будет ждать лишнее время.
Вариант с каналом результата позволяет дождаться конкретного завершения и получить ошибку, но для нескольких независимых операций потребуется координировать несколько каналов. WaitGroup проще, когда нужен только факт окончания всех записей, однако ошибки придётся передавать отдельно.
В этой ситуации выбирают группу задач с ожиданием и сбором ошибок: main не завершается, пока отчёт не записан, а ошибка записи возвращается пользователю с ненулевым кодом завершения. В результате поведение становится детерминированным, а задержка ограничивается реальным временем записи, а не произвольным таймером.
1. Выполняются ли defer незавершённой горутины при завершении main?
Нет гарантии. defer выполняются при обычном выходе из функции данной горутины, но завершение процесса из-за возврата main не является корректным завершением каждой горутины. Поэтому критически важное освобождение ресурсов нельзя оставлять только на случай завершения фоновой горутины.
2. Достаточно ли вызвать runtime.Gosched, чтобы горутина успела выполниться?
Нет. runtime.Gosched может уступить процессор текущей горутиной, но не устанавливает условия завершения другой горутины и не создаёт ожидания. Планировщик может выбрать другой порядок выполнения, поэтому для гарантии нужен явный протокол синхронизации.
3. Нужно ли ждать горутины после закрытия канала?
Закрытие канала само по себе не означает, что отправитель уже завершил работу. Оно лишь запрещает дальнейшие отправки и позволяет получателям узнать об окончании потока значений. Если требуется гарантировать завершение самой горутины, дополнительно нужен WaitGroup, сигнал завершения или другой механизм ожидания.