31 вопросов
Горутина - это легковесный поток выполнения в Go, управляемый рантаймом. Запускается ключевым словом go перед вызовом функции. Не является потоком ОС - множество горутин могут выполняться на одном потоке.
go func() { fmt.Println("hello") }()Горутина представлена структурой g в рантайме. Изначальный размер стека - около 2 КБ (зависит от версии). Стек растет и сжимается динамически. Указатель на текущую инструкцию, стек, канал ожидания и прочие метаданные хранятся в g.
Горутины легче: маленький стек, быстрый запуск и переключение. Потоки ОС - тяжелые (мегабайты стека, дорогой контекст). Рантайм Go планирует множество горутин на небольшое число потоков ОС (M). Переключение горутин без перехода в ядро.
Дешевое создание и переключение, возможность держать сотни тысяч горутин в одном процессе. Простая модель: go f(). Хорошая интеграция с каналами и планировщиком. Меньше накладных расходов, чем у потоков.
Нет приоритетов и жёстких гарантий времени выполнения. Блокирующий вызов в горутине блокирует поток M. Сложнее отладка при большом числе горутин. Паника в горутине по умолчанию роняет всю программу, если её не ловить.
Планировщик рантайма Go (scheduler). Он распределяет горутины (G) по потокам ОС (M), привязанным к процессорам (P). Планировщик выполняет work stealing, преemption и интеграцию с сетевым поллером. Пользователь не управляет потоками напрямую.
Дать горутине возможность выйти по условию: по каналу отмены (done), по контексту или по закрытию канала. Не убивать горутины принудительно. Дождаться выхода через WaitGroup, канал или errgroup.
done := make(chan struct{})
go func() { defer close(done); doWork() }()
// ...
<-doneДа, если есть несколько ядер и GOMAXPROCS > 1. Несколько горутин могут выполняться одновременно на разных ядрах. При GOMAXPROCS=1 параллелизма нет - только конкурентность (переключение по квантам и при блокировке).
Планировщик привязывает логические процессоры (P) к ядрам. На каждом P выполняется одна горутина в момент времени. Количество активных P по умолчанию равно числу ядер (GOMAXPROCS). Больше горутин - они делят время на этих P.
Жёсткого лимита в языке нет. Ограничения - память (стек и метаданные каждой горутины) и практическая нагрузка на планировщик. Сотни тысяч горутин возможны; миллионы уже нагружают планировщик и потребляют много памяти.
По умолчанию лимит порядка гигабайта (зависит от платформы). Стек растет при нехватке места. В старых версиях был сегментированный стек с копированием при росте; сейчас непрерывный стек с резервированием.
Стек и структура g выделяются из кучи (heap) рантаймом. При создании горутины выделяется начальный стек. При росте стека может выделяться новый блок и копироваться данные (в зависимости от реализации).
Переключение контекста (context switch) - когда планировщик снимает с выполнения одну горутину и ставит другую на тот же поток M. У горутин оно дешевое: сохраняются только регистры и указатель стека, без перехода в ядро ОС.
Меньший размер стека, быстрее создание и переключение. Переключение полностью в пользовательском пространстве. Планировщик оптимизирован под много горутин. Потоки ОС требуют системных вызовов и больших стеков.
Передавать ошибки через канал в основную горутину или использовать errgroup. Не полагаться на панику - перехватывать и превращать в ошибку или передавать в канал. Результат и ошибку можно вернуть через один канал структур.
type result struct { v int; err error }
ch := make(chan result, 1)
go func() { v, err := do(); ch <- result{v, err} }()
r := <-chЧерез один канал, в который пишут все горутины; читать в цикле или через 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) }Паника распространяется по стеку горутины. Если её не восстановить через recover(), горутина завершается и по умолчанию паника не передаётся в другие горутины - но если непойманная паника доходит до корня горутины, программа падает.
go func() {
defer func() { if r := recover(); r != nil { log.Println(r) } }()
panic("oops")
}()Блокирующая операция (системный вызов, чтение/запись канала без готовности) блокирует горутину. Планировщик отвязывает эту горутину от потока M и может выполнять другие горутины на этом M. Поток блокируется только если все горутины на нём заблокированы.
Нет, в Go нет API для убийства горутины. Нужно спроектировать код так, чтобы горутина сама выходила: по каналу отмены, контексту или закрытию канала. Иначе возможны утечки и повреждение данных при принудительном обрыве.
Через 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()Использовать пул воркеров (ограниченное число горутин), которые берут задачи из канала. Либо семафор через буферизованный канал для ограничения параллелизма. Не запускать 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)Горутина запущена и никогда не завершается: ждёт на канале, с которого никто не пишет, или в бесконечном цикле без выхода. Утечка потребляет память и может исчерпать ресурсы. Избегать: всегда предусматривать выход по отмене/закрытию канала.
Передать контекст с отменой или канал 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()Фиксированное число горутин-воркеров обрабатывают задачи из общего канала. Задачи кладут в канал, воркеры читают и выполняют. Ограничивает параллелизм и число горутин. Классический паттерн для массовой обработки.
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)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 } }()Цепочка этапов: каждый этап - горутина, читает из входного канала, обрабатывает, пишет в выходной. Данные потоком идут по конвейеру. Классика: генератор - этап - этап - потребитель. Закрытие каналов распространяется по цепочке.
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) }Буферизованный канал с ёмкостью n: отправка - захват, приём - освобождение. Ограничивает число одновременных операций.
sem := make(chan struct{}, 10)
sem <- struct{}{} // acquire
defer func() { <-sem }() // release
doWork()В корне горутины вызвать defer recover() и обработать: залогировать, отправить ошибку в канал или в errgroup. Иначе непойманная паника завершит программу. Не восстанавливать панику "молча" - хотя бы логировать.
go func() {
defer func() { if r := recover(); r != nil { errCh <- fmt.Errorf("panic: %v", r) } }()
doWork()
}()В Go стек горутины динамический: начинается с малого размера и при необходимости растёт (и при очень большом неиспользовании может сжиматься). Статический стек - фиксированный размер у потоков ОС. Динамический экономит память при большом числе горутин.
Идея: дочерние задачи привязаны к жизненному циклу родителя и не переживают его. В Go приближают через контекст: отмена родителя отменяет детей; errgroup и контекст дают "дерево" отмен. Явное ожидание (Wait) и отмена вместо "запустил и забыл".
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 }