ПрограммированиеBackend разработчик

Объясните, что такое горутина в Go. Как она работает под капотом и чем отличается от потоков в других языках?

Проходите собеседования с ИИ помощником Hintsage

Ответ

Горутина — это легковесный поток выполнения, управляемый рантаймом Go. Для запуска новой горутины используют ключевое слово go перед вызовом функции. Под капотом создается структура, описывающая стек и состояние задачи, которая добавляется в очередь планировщика горутин.

В отличие от потоков ОС, горутина имеет гораздо меньший начальный стек (обычно 2 КБ), и стек автоматически расширяется при необходимости. Планировщик Go сам распределяет их по доступным потокам операционной системы (M:N-модель).

Основные отличия от потоков (threads):

  • Создаются быстрее и требуют меньше памяти
  • Планируются Go-рантаймом, а не ОС
  • Автоматически масштабируются по ядрам
  • Синхронизация реализована через каналы, что предотвращает многие классы гонок

Пример использования горутин и каналов:

package main import ( "fmt" "time" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker %d started job %d ", id, j) time.Sleep(time.Second) fmt.Printf("worker %d finished job %d ", id, j) results <- j * 2 } } func main() { jobs := make(chan int, 5) results := make(chan int, 5) for w := 1; w <= 3; w++ { go worker(w, jobs, results) } for j := 1; j <= 5; j++ { jobs <- j } close(jobs) for a := 1; a <= 5; a++ { <-results } }

Вопрос с подвохом

Могут ли горутины Go выполняться параллельно на нескольких ядрах?

Часто встречается неверный ответ: "Нет, потому что Go использует зеленые потоки". На самом деле, с помощью переменной окружения или вызова runtime.GOMAXPROCS(n) Go может распараллеливать выполнение горутин на всех доступных ядрах процессора.

Пример:

import "runtime" func main() { runtime.GOMAXPROCS(4) // Позволяет использовать 4 ядра ... }

Примеры реальных ошибок из-за незнания тонкостей темы


История

В проекте backend-сервиса на Go был реализован пул воркеров через горутины, но программисты забыли ограничить количество одновременно работающих горутин. В результате при увеличении нагрузки приложение запускало тысячи горутин, что привело к исчерпанию памяти и краху сервиса. Проблема решилась введением лимита активных горутин (например, с помощью семафора или worker pool).


История

Один из сотрудников неверно синхронизировал данные между горутинами, используя обычные глобальные переменные без мьютексов или каналов. Это вызвало гонку данных (race condition), из-за чего периодически происходили ошибки при обработке платежей. Проблема была обнаружена только после запуска в production.


История

В сервисе парсинга был упущен момент передачи nil-каналов в select: после закрытия канала select продолжал заблокированно ждать данные, и часть горутин "повисала". Исправили присваиванием nil закрытому каналу и правильной обработкой select.