Go: Горутины

31 вопросов

1 Что такое горутина?

Горутина - это легковесный поток выполнения в Go, управляемый рантаймом. Запускается ключевым словом go перед вызовом функции. Не является потоком ОС - множество горутин могут выполняться на одном потоке.

go func() { fmt.Println("hello") }()
Открыть отдельно →
2 Внутреннее устройство горутины, размер стека?

Горутина представлена структурой g в рантайме. Изначальный размер стека - около 2 КБ (зависит от версии). Стек растет и сжимается динамически. Указатель на текущую инструкцию, стек, канал ожидания и прочие метаданные хранятся в g.

Открыть отдельно →
3 Чем горутины отличаются от потоков ОС?

Горутины легче: маленький стек, быстрый запуск и переключение. Потоки ОС - тяжелые (мегабайты стека, дорогой контекст). Рантайм Go планирует множество горутин на небольшое число потоков ОС (M). Переключение горутин без перехода в ядро.

Открыть отдельно →
4 Какие преимущества горутин?

Дешевое создание и переключение, возможность держать сотни тысяч горутин в одном процессе. Простая модель: go f(). Хорошая интеграция с каналами и планировщиком. Меньше накладных расходов, чем у потоков.

Открыть отдельно →
5 Какие недостатки горутин?

Нет приоритетов и жёстких гарантий времени выполнения. Блокирующий вызов в горутине блокирует поток M. Сложнее отладка при большом числе горутин. Паника в горутине по умолчанию роняет всю программу, если её не ловить.

Открыть отдельно →
6 Кто управляет горутинами?

Планировщик рантайма Go (scheduler). Он распределяет горутины (G) по потокам ОС (M), привязанным к процессорам (P). Планировщик выполняет work stealing, преemption и интеграцию с сетевым поллером. Пользователь не управляет потоками напрямую.

Открыть отдельно →
7 Как правильно завершать горутины?

Дать горутине возможность выйти по условию: по каналу отмены (done), по контексту или по закрытию канала. Не убивать горутины принудительно. Дождаться выхода через WaitGroup, канал или errgroup.

done := make(chan struct{})
go func() { defer close(done); doWork() }()
// ...
<-done
Открыть отдельно →
8 Выполняются ли горутины параллельно?

Да, если есть несколько ядер и GOMAXPROCS > 1. Несколько горутин могут выполняться одновременно на разных ядрах. При GOMAXPROCS=1 параллелизма нет - только конкурентность (переключение по квантам и при блокировке).

Открыть отдельно →
9 Как горутины соотносятся с ядрами CPU?

Планировщик привязывает логические процессоры (P) к ядрам. На каждом P выполняется одна горутина в момент времени. Количество активных P по умолчанию равно числу ядер (GOMAXPROCS). Больше горутин - они делят время на этих P.

Открыть отдельно →
10 Есть ли лимит на количество горутин?

Жёсткого лимита в языке нет. Ограничения - память (стек и метаданные каждой горутины) и практическая нагрузка на планировщик. Сотни тысяч горутин возможны; миллионы уже нагружают планировщик и потребляют много памяти.

Открыть отдельно →
11 Какой максимальный размер стека горутины?

По умолчанию лимит порядка гигабайта (зависит от платформы). Стек растет при нехватке места. В старых версиях был сегментированный стек с копированием при росте; сейчас непрерывный стек с резервированием.

Открыть отдельно →
12 Откуда берётся память для горутины?

Стек и структура g выделяются из кучи (heap) рантаймом. При создании горутины выделяется начальный стек. При росте стека может выделяться новый блок и копироваться данные (в зависимости от реализации).

Открыть отдельно →
13 Что такое переключение контекста у горутин?

Переключение контекста (context switch) - когда планировщик снимает с выполнения одну горутину и ставит другую на тот же поток M. У горутин оно дешевое: сохраняются только регистры и указатель стека, без перехода в ядро ОС.

Открыть отдельно →
14 Почему горутины быстрее потоков?

Меньший размер стека, быстрее создание и переключение. Переключение полностью в пользовательском пространстве. Планировщик оптимизирован под много горутин. Потоки ОС требуют системных вызовов и больших стеков.

Открыть отдельно →
15 Как обрабатывать ошибки из горутин?

Передавать ошибки через канал в основную горутину или использовать errgroup. Не полагаться на панику - перехватывать и превращать в ошибку или передавать в канал. Результат и ошибку можно вернуть через один канал структур.

type result struct { v int; err error }
ch := make(chan result, 1)
go func() { v, err := do(); ch <- result{v, err} }()
r := <-ch
Открыть отдельно →
16 Как собирать результаты из нескольких горутин?

Через один канал, в который пишут все горутины; читать в цикле или через for range до закрытия. Либо использовать WaitGroup и общую структуру с мьютексом. Либо errgroup с общим слайсом результатов (с защитой мьютексом).

ch := make(chan int, N)
for i := 0; i < N; i++ { go func(i int) { ch <- work(i) }(i) }
for i := 0; i < N; i++ { results = append(results, <-ch) }
Открыть отдельно →
17 Что происходит при панике в горутине?

Паника распространяется по стеку горутины. Если её не восстановить через recover(), горутина завершается и по умолчанию паника не передаётся в другие горутины - но если непойманная паника доходит до корня горутины, программа падает.

go func() {
    defer func() { if r := recover(); r != nil { log.Println(r) } }()
    panic("oops")
}()
Открыть отдельно →
18 Блокирует ли горутина поток ОС?

Блокирующая операция (системный вызов, чтение/запись канала без готовности) блокирует горутину. Планировщик отвязывает эту горутину от потока M и может выполнять другие горутины на этом M. Поток блокируется только если все горутины на нём заблокированы.

Открыть отдельно →
19 Можно ли принудительно завершить горутину?

Нет, в Go нет API для убийства горутины. Нужно спроектировать код так, чтобы горутина сама выходила: по каналу отмены, контексту или закрытию канала. Иначе возможны утечки и повреждение данных при принудительном обрыве.

Открыть отдельно →
20 Как дождаться завершения горутин?

Через sync.WaitGroup: перед запуском wg.Add(n), в горутине в конце defer wg.Done(), затем wg.Wait(). Либо канал: горутина в конце пишет в канал или закрывает его, основная читает. Либо errgroup.Group.

var wg sync.WaitGroup
for i := 0; i < n; i++ { wg.Add(1); go func() { defer wg.Done(); work() }() }
wg.Wait()
Открыть отдельно →
21 Как управлять 100 000 горутин?

Использовать пул воркеров (ограниченное число горутин), которые берут задачи из канала. Либо семафор через буферизованный канал для ограничения параллелизма. Не запускать 100k горутин без ограничения - возможны нехватка памяти и перегрузка планировщика.

jobs := make(chan int, 100)
for w := 0; w < 1000; w++ { go func() { for j := range jobs { do(j) } }() }
for j := 0; j < 100000; j++ { jobs <- j }
close(jobs)
Открыть отдельно →
22 Что такое утечка горутин?

Горутина запущена и никогда не завершается: ждёт на канале, с которого никто не пишет, или в бесконечном цикле без выхода. Утечка потребляет память и может исчерпать ресурсы. Избегать: всегда предусматривать выход по отмене/закрытию канала.

Открыть отдельно →
23 Как сделать graceful shutdown горутин?

Передать контекст с отменой или канал done. При получении сигнала (SIGTERM) вызвать cancel() или закрыть done. Горутины проверяют ctx.Done() или <-done и выходят. Дождаться завершения через WaitGroup или errgroup.

ctx, cancel := context.WithCancel(context.Background())
go func() { <-sig; cancel() }()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return runServer(ctx) })
g.Wait()
Открыть отдельно →
24 Что такое пул воркеров (worker pool)?

Фиксированное число горутин-воркеров обрабатывают задачи из общего канала. Задачи кладут в канал, воркеры читают и выполняют. Ограничивает параллелизм и число горутин. Классический паттерн для массовой обработки.

jobs := make(chan Task, 100)
for w := 0; w < 10; w++ { go func() { for j := range jobs { j.Do() } }() }
for _, t := range tasks { jobs <- t }
close(jobs)
Открыть отдельно →
25 Что такое fan-in и fan-out?

Fan-out: одна горутина раздаёт работу нескольким воркерам (один канал читают много горутин или раздаём по каналам). Fan-in: несколько источников сливают данные в один канал (например, через select по нескольким каналам в один).

// fan-in: merge ch1, ch2 -> out
go func() { for v := range ch1 { out <- v } }()
go func() { for v := range ch2 { out <- v } }()
Открыть отдельно →
26 Что такое pipeline (конвейер)?

Цепочка этапов: каждый этап - горутина, читает из входного канала, обрабатывает, пишет в выходной. Данные потоком идут по конвейеру. Классика: генератор - этап - этап - потребитель. Закрытие каналов распространяется по цепочке.

gen := func() <-chan int { ch := make(chan int); go func() { for i := 0; i < n; i++ { ch <- i }; close(ch) }(); return ch }
sq := func(in <-chan int) <-chan int { out := make(chan int); go func() { for v := range in { out <- v*v }; close(out) }(); return out }
for v := range sq(gen()) { fmt.Println(v) }
Открыть отдельно →
27 Как реализовать семафор через канал?

Буферизованный канал с ёмкостью n: отправка - захват, приём - освобождение. Ограничивает число одновременных операций.

sem := make(chan struct{}, 10)
sem <- struct{}{}   // acquire
defer func() { <-sem }()  // release
doWork()
Открыть отдельно →
28 Что делать, если дочерняя горутина паникует?

В корне горутины вызвать defer recover() и обработать: залогировать, отправить ошибку в канал или в errgroup. Иначе непойманная паника завершит программу. Не восстанавливать панику "молча" - хотя бы логировать.

go func() {
    defer func() { if r := recover(); r != nil { errCh <- fmt.Errorf("panic: %v", r) } }()
    doWork()
}()
Открыть отдельно →
29 Динамический и статический стек горутины?

В Go стек горутины динамический: начинается с малого размера и при необходимости растёт (и при очень большом неиспользовании может сжиматься). Статический стек - фиксированный размер у потоков ОС. Динамический экономит память при большом числе горутин.

Открыть отдельно →
30 Что такое structured concurrency?

Идея: дочерние задачи привязаны к жизненному циклу родителя и не переживают его. В Go приближают через контекст: отмена родителя отменяет детей; errgroup и контекст дают "дерево" отмен. Явное ожидание (Wait) и отмена вместо "запустил и забыл".

Открыть отдельно →
31 Для чего нужен errgroup.WithContext?

g, ctx := errgroup.WithContext(ctx) создаёт группу горутин с общим контекстом: при первой возвращённой ошибке контекст отменяется и остальные горутины получают отмену. g.Go(f) запускает горутины, g.Wait() ждёт и возвращает первую ошибку. Удобно для параллельных операций с отменой.

g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error { return fetch(ctx, url1) })
g.Go(func() error { return fetch(ctx, url2) })
if err := g.Wait(); err != nil { return err }
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.