Горутины: лёгкие потоки Go
Разбираем вопрос, с которого почти всегда начинается Go-секция интервью: что такое горутина и чем она отличается от обычного потока.
Горутина (goroutine) — это лёгкая функция, которую планировщик Go выполняет параллельно с остальной программой. Горутины живут внутри одного или нескольких потоков ОС, но сами потоками не являются — их создание почти ничего не стоит.
Вопрос: чем горутина отличается от потока операционной системы?
Чёткий ответ. Поток ОС — это дорогая штука: под него ядро выделяет память под стек (обычно 1–8 мегабайт) и тратит время на переключение контекста. Горутина — это структура внутри самого Go-рантайма. Стартовый стек горутины — всего около 2 килобайт, и он растёт по мере необходимости. Поэтому в одной программе можно спокойно запустить сотни тысяч горутин, а сопоставимое число потоков ОС положит любую систему.
За горутинами следит планировщик Go (модель "M:N"): он сам решает, на каком из небольшого числа реальных потоков ОС в данный момент выполнить ту или иную горутину, и умеет переключаться между ними, когда одна из них ждёт, например, ответа от сети. Разработчику эту логику писать не нужно — рантайм берёт её на себя.
Как запустить горутину
Синтаксис нарочно предельно простой: перед вызовом функции ставится ключевое слово go. Функция начинает выполняться "в фоне", а строка кода сразу после неё продолжает работать, не дожидаясь её завершения.
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Привет из горутины")
}
func main() {
go sayHello() // запустили и не ждём
fmt.Println("Привет из main")
time.Sleep(100 * time.Millisecond) // даём горутине шанс отработать
}
Вывод:
Привет из main
Привет из горутины
Обратите внимание на порядок строк: он не гарантирован. main и горутина выполняются независимо, поэтому "Привет из main" вполне может напечататься и до, и после "Привет из горутины" — это зависит от того, как планировщик распределит время между ними. На собеседовании это частый вопрос-ловушка: неопытный кандидат отвечает "выведется в таком порядке, как написано в коде", хотя гарантий порядка тут нет.
А вот time.Sleep в примере — это костыль исключительно для демонстрации, в реальном коде так синхронизацию не делают: программа может завершиться раньше, чем горутина успеет отработать, потому что main не ждёт горутины сама по себе. Как ждать по-настоящему — разберём в следующем уроке (там появится sync.WaitGroup).
Классическая ловушка: захват переменной цикла
Это едва ли не самый частый вопрос про горутины на собеседовании. Представьте, что вы запускаете несколько горутин в цикле, и каждая должна напечатать "свой" номер итерации.
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // ЛОВУШКА: берём i из внешней области
}()
}
time.Sleep(100 * time.Millisecond)
}
Вывод (в старых версиях Go, до 1.22):
3
3
3
Почему так? Все три горутины замыкают одну и ту же переменную i, а не её значение на момент запуска. Пока горутины реально доберутся до fmt.Println, цикл в main уже успевает досчитать до конца, и i становится равна 3 для всех трёх горутин сразу — вот почему все они печатают одно и то же число.
Есть два способа это починить:
for i := 0; i < 3; i++ {
i := i // делаем локальную копию на каждой итерации
go func() {
fmt.Println(i)
}()
}
// или передать значение параметром — так даже нагляднее
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
Вывод (порядок строк может отличаться, но значения всегда 0, 1, 2):
0
1
2
Важная деталь для собеседования: начиная с Go 1.22 язык изменил семантику — теперь у каждой итерации цикла своя собственная переменная i, и старая ловушка сама по себе исчезает. Но вопрос про неё всё равно продолжают задавать: важно понимать саму механику захвата переменных по ссылке, а не просто помнить "в новых версиях уже пофиксили".
Частые ошибки на собеседовании
- Путают горутину с потоком ОС и говорят, что она "тяжёлая" — на деле горутины стартуют с крошечным стеком.
- Считают, что
go func()гарантирует какой-то порядок выполнения относительно остального кода — не гарантирует. - Не могут объяснить ловушку захвата переменной цикла или не знают, что в Go 1.22+ поведение изменилось.
- Используют
time.Sleepкак "решение" проблемы ожидания горутин в реальном коде — это не синхронизация, а угадывание времени.
Итог
- Горутина — лёгкая единица параллелизма Go, управляемая рантаймом, а не ОС.
- Запускается ключевым словом
goперед вызовом функции; выполнение продолжается сразу же, не дожидаясь горутины. - Порядок выполнения горутин не гарантирован.
- Переменная цикла, захваченная замыканием, до Go 1.22 была общей на все итерации — классическая ловушка на собеседовании.