Node.js раскрывает один тред и в рамках этого треда оперирует ассихнронными задачами: когда мы достигаем момента IO или syscall нода ставит задачу в очередь ожидания завершения IO / syscall и берется за другую с завершенным IO / syscall. Каждая подобная задача сохраняет свой стэк, ссылки замыкания и "позицию в коде" с которой надо продолжить.
Условное сравнение – так работает 1 ядро процессора: оно выполняет операции до момента, когда сталкивается с IO / syscall откладывает задачу до момента пока она не сообщит "я получила все нужные данные"
Golang же создает кол-во тредов прописанное в енве GOMAXPROCS, создает на каждом из них "воркера" ("процессор"), сущность похожую по факту на весь Node.js, и точно также иполняет задачи (goroutine) до IO / syscall и откладывает продолжение до готовности
И если сравнивать тут, то это похоже на устройство совокупности ядер, где scheduler разбрасывает задачи между ними, а каждое отлельное ядро обрабатывает или откладывает задачи ожидающие IO / syscall
Важные отличия в том, что (1) горутины исполняются параллельно в рамках набора тредов и последовательно в рамках треда, (2) go умеет перекидывать goroutine между тредами, если какой-то освободился раньше других, (3) в нем есть preemptive scheduler, который выдает даже синхронной горутине время на исполнение не дольше 10ms, позволяя не беспокоится, что длинные синхронные горутины будут блокировать исполнение других
Также, интересно отметить, что подобных механизм является некоторой "золотой серединой" в задачах максимизации исполнения IO intensive задач, который может быть также реализован и на других языках и самое близкое к горутинам понятие это "green threads". Несомненно, существуют кейсы, когда нам важно жестко цеплять задачи к конкретным тредам и ядрам (например, thread per core), но это уже прерогатива других языков.
Из сходств с нодой то, что мы практически никак не управляем тредами, конкурентностью и параллельностью, заисключением пары редкоиспользуемых механик
Основных отличия в программировании на Node.js и Go с учетом данного механизма 2 – (1) если какие-то данные мутабельно используются между несколькими goroutine, то у нас есть все шансы на рейсинг в связи с чем,
приходится применять разные техники разруливания конкурентности (атомики, локи, иммутабельность, акторы, и т.д.), (2) для безопасной коммуникации между горутинами
надо использовать каналы (channels)Даже на Node.js можно сталкиваться с рейсингом (например, попробуйте мутировать одни и теже данные в Promise.all), но на ноде достаточно редкий кейс, поэтому мы не так часто им пользуемся
Поэтому основные темы, которые надо взять после ноды: WaitGroup, ErrGroup , Atomic, RWLock, fan-in, fan-out, buffered vs unbuffered channels
Оставлю пару полезных ссылок (лучше читать поочереди):
-
https://morsmachine.dk/go-scheduler-
https://www.youtube.com/watch?v=rloqQY9CT8I-
https://www.youtube.com/watch?v=KBZlN0izeiY&t=536s-
https://stackoverflow.com/questions/73915144/why-is-go-considered-partially-preemptive-
https://shubhagr.medium.com/internals-of-go-channels-cf5eb15858fc-
https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html