Что такое goroutine? Чем она отличается от потока ОС?
Горутина — легковесный поток выполнения в user space; стартует с ~2 KB стека против ~1 MB у потока ОС и создаётся за наносекунды. Рантайм Go мультиплексирует горутины на потоки ОС через планировщик M:N с work stealing.
Goroutine — легковесный поток выполнения
Горутина — единица конкурентности в Go. Запустить её можно ключевым словом go перед вызовом функции. Рантайм Go мультиплексирует тысячи (и даже миллионы) горутин на небольшое число потоков ОС через планировщик M:N.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("worker %d starting\n", id)
// ...работа...
fmt.Printf("worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("all workers finished")
}
Чем горутина отличается от потока ОС
- Размер стека: поток ОС требует фиксированного стека ~1–8 MB. Горутина стартует с ~2–8 KB и растёт динамически по мере необходимости — это позволяет держать миллионы горутин.
- Стоимость создания: создание потока ОС — системный вызов, ~10–100 мкс. Создание горутины — несколько сотен наносекунд.
- Переключение контекста: переключение между потоками ОС требует перехода в kernel space и сохранения/восстановления регистров CPU (~1–10 мкс). Переключение горутин управляется в user space планировщиком Go и занимает ~100–200 нс.
- Планирование: потоки ОС планирует ядро (preemptive). Горутины планирует рантайм Go (cooperative + asynchronous preemption с Go 1.14).
Планировщик Go: модель M:N
Планировщик работает с тремя сущностями:
- G (goroutine) — горутина с её стеком и состоянием.
- M (machine) — поток ОС, выполняющий горутины.
- P (processor) — логический процессор, очередь горутин и контекст выполнения. Количество P =
GOMAXPROCS(по умолчанию = числу CPU).
Каждый P держит локальную runqueue горутин. Когда P простаивает, он «крадёт» горутины из других P (work stealing), что обеспечивает равномерную загрузку ядер.
import "runtime"
func main() {
// Узнать текущий GOMAXPROCS
fmt.Println(runtime.GOMAXPROCS(0)) // 0 = только прочитать
// Явно задать (обычно не нужно — Go сам определяет по числу CPU)
runtime.GOMAXPROCS(4)
// Уступить текущему планировщику (редкий приём)
runtime.Gosched()
// Количество горутин сейчас
fmt.Println(runtime.NumGoroutine())
}
Коммуникация между горутинами
Идиома Go: «Не коммуницируй через общую память — разделяй память через коммуникацию». Основной инструмент — каналы (chan).
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func main() {
for n := range generate(2, 3, 5, 7) {
fmt.Println(n)
}
}
Подводные камни
- Утечка горутин: горутина, заблокированная на канале или IO без таймаута, живёт вечно. Всегда используйте
context.Contextс дедлайном или отменой. - Захват переменной цикла: до Go 1.22 переменная
iвfor i := range ...была общей для всех итераций — все горутины видели последнее значение. В Go 1.22 семантика исправлена, но код под старые версии требует явной копии:i := i. - Горутина не возвращает значение: результат нужно передавать через канал или сохранять в разделяемую переменную под мьютексом.
- panic в горутине роняет весь процесс: recover() работает только внутри той же горутины; каждая горутина должна защищаться сама.
- GOMAXPROCS=1 скрывает гонки: на однопоточном планировщике гонки данных могут не проявляться; всегда запускайте тесты с
-race. - sync.WaitGroup.Add до go: вызов
wg.Add(1)после старта горутины создаёт гонку —Addдолжен быть передgo.
Common mistakes
- Называть goroutine «зелёным потоком» и на этом останавливать объяснение.
- Игнорировать связь goroutine с scheduler, GOMAXPROCS и блокирующими вызовами.
- Не упоминать стоимость утечек и отсутствие автоматической отмены.
What the interviewer is testing
- Отличает goroutine от OS thread по роли runtime, стеку и стоимости переключения.
- Понимает, что параллельность ограничивается GOMAXPROCS и доступными CPU.
- Связывает ответ с каналами, context или WaitGroup в реальном коде.