Практическое руководство по работе с горутинами в Go с примерами sync.WaitGroup, sync.Mutex, context и sync.Pool. Около 60–90 минут выполнения для полного прогона примеров.
Вы получите рабочие примеры использования горутин и синхронизации в Go, готовые к запуску на локальной машине. Примеры включают WaitGroup, Mutex, context-cancel и sync.Pool; полный прогон занимает примерно 60–90 минут в зависимости от скорости машины.
Что вы изучите
Базовый запуск горутины и причина, почему main может завершить программу раньше.
Использование sync.WaitGroup для ожидания задач и предотвращения гонок данных.
Защита общих ресурсов с sync.Mutex и обнаружение race с -race.
Отмена горутин через context.Context и шаблоны предотвращения утечек.
Переиспользование буферов с sync.Pool для снижения аллокаций под нагрузкой.
Требования
Go 1.23 (релиз 2025) — компилятор и runtime для запуска примеров.
ОС: Linux (Ubuntu 22.04/24.04), macOS 13+, Windows 10+.
Минимум 2 vCPU и 2 ГБ ОЗУ для локальных проб и до 8 ГБ ОЗУ для нагрузочного теста.
Дополнительно: go в PATH, опционально pprof для профилирования.
Что такое горутина?
0
Статья была полезной?
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…
Горутина — легковесный поток управления в языке Go, запущенный ключевым словом go. В отличие от системных потоков, горутины имеют стартовый стек порядка ~2KB и растут/сжимаются по мере необходимости, что позволяет запускать десятки и сотни тысяч горутин в одном процессе при достаточном объёме памяти. Горутины выполняются планировщиком runtime Go, который мультиплексирует их на системные OS-потоки.
Когда использовать?
Горутины эффективны для I/O-bound задач (сетевые запросы, файловые операции), параллельной обработки задач и распределения работы между воркерами. Для CPU-bound задач полезно комбинировать горутины с GOMAXPROCS. Практические числа: 100–10000 горутин безопасно на машине с 4 ядрами и 8 ГБ ОЗУ при небольшом контекстном состоянии каждой; миллионы горутин требуют значительной памяти и тщательного профилирования. Для обмена данными используйте каналы или sync-примитивы; избегайте глобального состояния без синхронизации.
Как избежать утечек?
Утечка горутины — состояние, когда горутина блокируется навсегда и не завершается. Основные причины: ожидание на канал без отправителя, забытый канал отмены, блокировка на внешнем ресурсе. Профилактика: явная отмена через context.Context, использование select с ctx.Done(), таймауты и аккуратное применение WaitGroup. Также применяйте инструменты: go test -race и pprof для поиска висящих стеков и утечек.
Шаг 1: Минимальный пример горутины и типовая ошибка
Команда: сохраните файл main.go и выполните go run main.go. Пример демонстрирует, как main может завершить программу раньше, чем завершаются горутины.
// main.go
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("hello from goroutine")
}()
// main завершается сразу
}
Типовая ошибка: забыли слушать ctx.Done(). Тогда горутины останутся висящими и будут потреблять ресурсы. Фикс: в каждой горутине всегда добавляйте ветвь для отмены через select и применяйте таймауты, если операция блокирующая.
Шаг 4: sync.Pool для уменьшения аллокаций
Команда: go run pool.go. Пример показывает переиспользование буферов для снижения давления на сборщик мусора при интенсивном создании объектов.
// pool.go
package main
import (
"bytes"
"fmt"
"sync"
)
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func worker(id int, n int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < n; i++ {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString(fmt.Sprintf("worker %d: %d", id, i))
// используем буфер
_ = b.String()
bufPool.Put(b)
}
}
func main() {
var wg sync.WaitGroup
workers := 50
items := 2000
wg.Add(workers)
for i := 0; i < workers; i++ {
go worker(i, items, &wg)
}
wg.Wait()
fmt.Println("done")
}
Ожидаемый вывод:
done
Типовая ошибка: неверно обрабатывать значение из Pool (например, привести к неверному типу) или положить в Pool объект в неконсистентном состоянии. Фикс: всегда вызывать Reset() и проверять приведение типов. Если New возвращает nil, и вы ожидаете не-nil, инициализируйте объект в New.
Шаг 5: Отладка — race detector и pprof
Команды для диагностики:
go run -race program.go — запуск с детектором гонок.
go test -run TestX -bench . -benchmem — бенчмарки с памятью.
Профилирование: собрать CPU-профиль и открыть его в pprof: go test -cpuprofile cpu.prof и go tool pprof cpu.prof.
Пример команды и возможный вывод race detector:
go run -race counter.go
==================
WARNING: DATA RACE
Read at 0x00c0000a2008 by goroutine 8:
main.main.func1()
/home/user/counter.go:15 +0x3a
Previous write at 0x00c0000a2008 by goroutine 7:
main.main.func1()
/home/user/counter.go:15 +0x5a
==================
Found 1 data race(s)
exit status 66
Типовая ошибка: отсутствие pprof или невозможность собрать профиль в production. Фикс: откройте доступ к профилю через защищённый endpoint, используйте сбор профилей в контролируемой среде или снимайте дампы стеков через runtime/pprof по расписанию.
Скриншот стека висящих горутин в pprof
Скрин вывода go run -race с сообщением data race
Используйте sync.WaitGroup для координации завершения.
Защищайте общие переменные sync.Mutex или атомиками.
Отменяйте долгоживущие горутины через context.
Снижайте аллокации с sync.Pool под нагрузкой.
Для практических руководств по деплою и системному запуску посмотрите материалы на Golang и по CI/CD на DevOps. Также полезны статьи о профилировании и оптимизации памяти.
Частые вопросы
Как определить, что горутина утекла?
Утечка горутины обычно проявляется как постоянный рост числа активных goroutine по времени или зависание сервиса. Для обнаружения снимайте профиль goroutine с помощью pprof (endpoint или runtime/pprof) и смотрите стек каждой висящей горутины. Если в стеке есть операции чтения с канала без отправителя или ожидание на внешнем ресурсе, это указывает на утечку. Также используйте go test -race, чтобы исключить гонки, которые могут привести к незавершению работы.
Что легче: каналы или sync.Mutex?
Выбор зависит от задачи. Каналы хороши для передачи данных и построения конвейеров; они делают код декларативным и помогают избежать явных блокировок. sync.Mutex эффективен для простой защиты небольшого участка кода или структуры данных; он быстрее, когда требуется низкий оверхед и простая критическая секция. Под высокими нагрузками замеряйте оба подхода и используйте инструменты профилирования для принятия решения.
Почему sync.Pool помогает с производительностью?
sync.Pool снижает число аллокаций и работу GC, переиспользуя объекты между горутинами. Это особенно заметно при короткоживущих объектах в hot-path. Однако Pool не гарантирует сохранение объектов между garbage collection'ами, поэтому не следует хранить состояние, которое важно для корректности; используйте Pool только для временных буферов и объектов, которые можно безопасно реинициализировать.
Когда нужно применять детектор гонок -race?
Запускайте -race на этапах тестирования и CI для обнаружения data race на ранней стадии. Детектор полезен при параллельных изменениях общих структур и при добавлении новых горутин. На production-сборках -race обычно не используют из-за ухудшения производительности, но его результаты обязаны лечиться до релиза.
Сколько горутин можно создать на одной машине?
Количество зависит от доступной памяти и потребления стека каждой горутиной. При стартовом стеке ~2KB теоретически можно создать сотни тысяч горутин на машине с достаточным объёмом ОЗУ. На практике важно профилировать: допустимо 100–10000 горутин для обычных сервисов; миллионы требуют специальной архитектуры и экономии памяти. Всегда измеряйте использование памяти и задержки при масштабировании.
Горутины в Go: практические примеры с sync | KtoHto
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…