Горутина это функция, которая выполняется конкурентно с остальными. Запуск: go f(x). Горутины дёшевы: стартовый стек около 2 КБ, он растёт и сжимается по необходимости, поэтому в программе спокойно живут сотни тысяч горутин.
Горутина против потока ОС
| Поток ОС | Горутина | |
|---|---|---|
| Стек | фиксированный, обычно мегабайты | от 2 КБ, растёт |
| Переключение | ядро ОС | рантайм Go в пространстве пользователя |
| Создание | дорогое | дешёвое |
Горутины выполняются на небольшом числе потоков ОС. Этим занимается планировщик рантайма.
Модель GMP
- G (goroutine): горутина со своим стеком и состоянием.
- M (machine): поток ОС, который исполняет код.
- P (processor): логический процессор, контекст для исполнения Go-кода. У каждого P своя локальная очередь готовых G (до 256). Число P задаёт
GOMAXPROCS.
Чтобы выполнять Go-код, M должен держать P. Схема работы:
- Новая горутина попадает в локальную очередь текущего P (в слот
runnext). - M берёт G из локальной очереди своего P. Периодически (примерно раз в 61 тик) заглядывает в глобальную очередь, чтобы там ничего не голодало.
- Если локальная очередь пуста: глобальная очередь, сетевой поллер, а затем work stealing: кража половины очереди у другого P.
- Если G ушла в блокирующий системный вызов, M блокируется вместе с ней, а P передаётся другому M (handoff), чтобы остальные горутины не стояли.
- Сетевой ввод-вывод не блокирует поток: горутина паркуется, а netpoller (epoll, kqueue) будит её, когда сокет готов.
Вытеснение
До Go 1.14 переключение было кооперативным: в точках вызова функций. Цикл без вызовов мог занять P навсегда. С Go 1.14 есть асинхронное вытеснение: монитор sysmon замечает горутину, которая работает дольше ~10 мс, и прерывает её сигналом.
GOMAXPROCS
По умолчанию равен числу логических CPU. В контейнере с лимитом CPU это было проблемой: Go видел все ядра узла и создавал слишком много P, что давало троттлинг. Раньше ставили go.uber.org/automaxprocs, а с Go 1.25 рантайм сам учитывает лимит CPU cgroup на Linux и обновляет значение, если лимит меняется.
Завершение и синхронизация
main не ждёт горутины: когда main возвращается, программа завершается, даже если горутины не доделали работу. Ждать нужно явно: sync.WaitGroup, каналы, errgroup.
var wg sync.WaitGroup
for i := range 3 {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i) // с Go 1.22 у каждой итерации свой i
}()
}
wg.Wait()
Порядок вывода не определён. С Go 1.22 переменная цикла создаётся заново на каждой итерации, и классический баг «все горутины печатают 3» ушёл, если в go.mod указана версия 1.22 или новее.
runtime.Gosched() уступает процессор, runtime.NumGoroutine() показывает число горутин. У горутин нет ID в публичном API, и убить горутину снаружи нельзя: она должна завершиться сама (через context или закрытие канала).
Утечки горутин
Горутина, навсегда заблокированная на канале, никогда не освободит память. Типичные причины: никто не читает из канала, никто не закрывает канал, по которому идёт range, нет отмены по context. Искать через pprof профиль goroutine или go.uber.org/goleak в тестах.
Частые ошибки
- Не ждать горутины и удивляться пустому выводу.
wg.Addвнутри горутины:Waitможет проскочить раньше.- Считать, что горутины выполнятся в порядке запуска.
- Запускать горутину без способа её остановить.
Что спросят на собесе
- Чем горутина отличается от потока.
- Как устроен планировщик: G, M, P, локальные и глобальная очереди, work stealing.
- Что происходит при блокирующем syscall и при сетевом вводе-выводе.
- Кооперативное или вытесняющее планирование в Go.
- Что такое GOMAXPROCS и что с ним в контейнерах.