Примеры использования goroutines в Go
Что проверяют
Понимаете ли вы, зачем нужны горутины и где они уместны, а не только как пишется go f(). Сильный ответ отличается тем, что кандидат сразу говорит про завершение, отмену и ограничение количества, а не только про запуск.
Как отвечать
Горутина это не поток операционной системы, а функция под управлением планировщика Go: стартовый стек в пару килобайт, тысячи горутин мультиплексируются на несколько системных потоков. Отсюда типовые сценарии:
- по горутине на входящее соединение или запрос,
net/httpтак и делает сам; - веерный запуск независимых обращений к внешним сервисам и сбор результатов;
- фоновые задачи: периодический сброс буфера, прогрев кэша, воркер над очередью;
- конвейер стадий, соединённых каналами.
Веер с ограничением параллелизма:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, u := range urls {
g.Go(func() error { return fetch(ctx, u) })
}
err := g.Wait()Главная мысль: у каждой горутины есть владелец, который дожидается её через sync.WaitGroup или errgroup, и способ её остановить через context. Неограниченный go в цикле по внешним данным это не параллелизм, а способ уронить и себя, и соседний сервис. Про переменную цикла тоже стоит сказать: начиная с Go 1.22 она своя на каждой итерации, в старом коде её копировали вручную.
Где спотыкаются
Забывают про утечку горутин. Горутина, которая пишет в канал, откуда больше никто не читает, висит вечно, и вместе с ней живёт всё, что она замкнула. Признак: runtime.NumGoroutine растёт и не возвращается к базовому уровню.
Вызывают wg.Add(1) внутри самой горутины. Планировщик не обязан запустить её немедленно, поэтому Wait может пройти до увеличения счётчика. Add вызывается до go, а сама WaitGroup передаётся только указателем.
Показывают пример с HTTP и не закрывают тело ответа. Без defer resp.Body.Close() соединения не возвращаются в пул, и веер из тысячи горутин упирается в лимит файловых дескрипторов быстрее, чем в сеть.