В Go ты пишешь максимально прямолинейно: открыл соединение, читаешь данные, ждешь.
Выглядит как обычный
blocking I/O. Но если бы это было правдой, любой highload сервис умирал бы уже на нескольких тысячах соединений.Под капотом работает netpoller. Это часть runtime, которая превращает твой “блокирующий” код в неблокирующую систему с event loop логикой.
Когда goroutine делает conn.Read(), она не блокирует поток. Если данных нет, runtime просто паркует goroutine и освобождает поток для других задач. В этот момент файловый дескриптор уходит в epoll, kqueue или IOCP в зависимости от системы.
Как только ОС говорит “данные готовы”, netpoller будит нужную goroutine и возвращает ее в scheduler. Всё. Никаких потоков на каждое соединение, никакого оверхеда на контекст-свитчи.
И вот тут главный трюк Go. Ты пишешь код как синхронный. Но исполняется он как асинхронный. Без колбэков, без promise, без ручного управления состоянием.
По сути это комбинация scheduler + event loop, спрятанная внутри runtime. Ты не видишь сложность, но получаешь масштабируемость.
Именно поэтому тысячи goroutines могут одновременно ждать I/O почти бесплатно. Каждая занимает килобайты памяти, а не мегабайты как поток.
Но есть нюанс. Netpoller это не бесконечная магия. При экстремальной нагрузке он сам может стать узким местом. Один poll loop, огромное количество событий и привет, рост latency на хвостах.
В итоге Go делает редкий финт. Даёт тебе простой синхронный код и сам превращает его в высоконагруженную асинхронную систему.
https://internals-for-interns.com/posts/go-netpoller/
