Горутины в Go
Основные концепции
Горутина — это легковесный поток выполнения в Go, который управляется рантаймом языка, а не операционной системой. В отличие от потоков ОС, горутины имеют меньший начальный размер стека (несколько КБ вместо 1-8 МБ) и динамически расширяют его по мере необходимости.
Ключевые характеристики:
- Запускаются с помощью ключевого слова
go - Выполняются конкурентно (concurrently), но не обязательно параллельно (parallel)
- Планируются рантаймом Go с помощью M:N планировщика (M горутин на N потоков ОС)
- Общаются через каналы, что реализует принцип "Do not communicate by sharing memory; instead, share memory by communicating"
Практические детали
Создание и завершение:
go func() {
// код горутины
}()
// горутина завершается, когда завершается её функция
Синхронизация:
- Каналы (
chan) — основной механизм синхронизации и передачи данных sync.WaitGroup— для ожидания завершения группы горутинsync.Mutex,sync.RWMutex— для защиты разделяемых данныхsync.Once— для однократного выполнения
Контекст и отмена:
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
// отмена через cancel()
Планировщик (GMP модель)
Go использует модель G-M-P:
- G (Goroutine) — горутина
- M (Machine) — поток ОС
- P (Processor) — логический процессор, выделяющий ресурсы для выполнения горутин
Количество P по умолчанию равно GOMAXPROCS (обычно = количеству ядер CPU). Каждый P имеет локальную очередь горутин. Когда P завершает выполнение горутины, он берёт следующую из своей очереди или "ворует" из очередей других P (work stealing).
Типичные проблемы и как их избежать
1. Утечки горутин Горутина не завершается, если она заблокирована на канале или ожидает данные, которые никогда не придут.
// Проблема: горутина никогда не получит данные
ch := make(chan int)
go func() {
val := <-ch // утечка
}()
// ch никогда не получает значение
Решение: использовать контекст с таймаутом или буферизированные каналы с select.
2. Гонка данных (data race) Доступ к общим данным без синхронизации.
counter := 0
for i := 0; i < 1000; i++ {
go func() {
counter++ // data race
}()
}
Решение: мьютексы, атомики или каналы.
3. Паника в горутине Паника в горутине приводит к падению всей программы.
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered: %v", r)
}
}()
// потенциально паникующий код
}()
4. Неправильное использование замыканий
// Проблема: все горутины увидят последнее значение i
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // все напечатают 10
}()
}
// Решение: передавать значение как аргумент
for i := 0; i < 10; i++ {
go func(val int) {
fmt.Println(val)
}(i)
}
Когда использовать горутины
Подходит:
- I/O-bound операции (HTTP запросы, работа с БД, чтение файлов)
- Обработка множества независимых задач
- Фоновые задачи (периодическая очистка кэша, мониторинг)
- Параллельная обработка данных (CPU-bound с ограничением через worker pool)
Не подходит:
- Очень короткие операции (оверхед создания горутины может превысить выгоду)
- Последовательные зависимости (лучше синхронный код)
Практические рекомендации
- Всегда контролируйте время жизни горутин — используйте
sync.WaitGroupилиerrgroup - Ограничивайте количество одновременно работающих горутин через worker pool или семафоры
- Используйте
selectс default для неблокирующих операций - Избегайте глобального состояния — передавайте зависимости явно
- Тестируйте с флагом
-raceдля обнаружения гонок данных - Логируйте создание и завершение критичных горутин для отладки
Горутины — мощный инструмент, но требуют дисциплины в управлении их жизненным циклом и синхронизации доступа к данным.