Горутины — это лёгкие потоки исполнения, заложенные в архитектуру Go c самых первых версий для достижения эффективной конкурентности. Исторически идея lightweight-thread появилась как попытка обойти дороговизну системных потоков, а также из-за высокой потребности в масштабируемых серверных приложениях. Go изначально проектировался как язык для серверных и сетевых систем, где миллионы задач должны обрабатываться параллельно.
Проблема: Какодействие может быстро привести к race conditions, дедлокам и росту потребления памяти, если не контролировать жизненный цикл горутин, не учитывать их планирование, а также не управлять завершением работы.
Решение: Горутины запускаются через ключевое слово go. Работа горутин планируется планировщиком Go, который использует модель M:N (M потоков ОС обслуживают N горутин языка Go). Для управления жизненным циклом применяют каналы, WaitGroup, context и контроль закрытия каналов.
Пример кода:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Worker %d started ", id) time.Sleep(time.Second) fmt.Printf("Worker %d done ", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Ключевые особенности:
Если в main горутину не дожидаться явно, всегда ли она выполнится?
Нет, выполнение main завершается — процесс завершится независимо от состояния дочерних горутин, и не все задачи будут выполнены.
Является ли запуск go func(...) из цикла гарантией, что каждая горутина получит собственное значение переменных цикла?
Нет, возникает проблемa захвата переменной цикла, горутины могут работать с одним и тем же значением среза/переменной. Нужно использовать копирование переменной, например, передавать как аргумент:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
Может ли одна горутина заблокировать планировщик Go и не дать выполниться другим?
Да, если в ней запускается бесконечный или очень тяжелый цикл без точек переключения (например, без вызовов функции времени или yield), она может удерживать поток ОС — хотя это противодействует идеологии Go о "кооперативном многозадачности". Например, тяжёлая функция без блокировок:
func busy() { for { // Нет никаких ожиданий или блокирующих вызовов } }
В микросервисе запускают периодически горутину чтения из базы данных, но забывают завершать её при отмене запроса. В результате остаются "висячие" горутины, которые со временем приводят к потреблению всей оперативной памяти.
Плюсы:
Минусы:
Используется context для контроля отмены задач, WaitGroup — для управления завершением всех горутин перед остановкой приложения, а каналы — для корректной передачи данных между исполнителями.
Плюсы:
Минусы: