netpoll 网络模型
问题
Go 的网络 IO 底层是怎么实现的?为什么 goroutine 阻塞在 Read 上不会浪费 OS 线程?
答案
Go 的网络 IO 模型
Go 网络 IO 的独特之处:代码看起来是同步阻塞的,底层却是异步非阻塞的。
核心机制
- 所有网络 fd 设置为非阻塞模式
- Read/Write 时如果未就绪,goroutine 被
gopark挂起(不占 OS 线程) - runtime 的 netpoll 使用操作系统的 IO 多路复用:
- Linux:
epoll - macOS:
kqueue - Windows:
IOCP
- Linux:
- 数据就绪时,netpoll 通过
goready唤醒对应 goroutine
与其他语言对比
| 模型 | 代表 | 开发体验 | 性能 |
|---|---|---|---|
| 同步阻塞 + 线程池 | Java BIO | 简单 | 差(线程重) |
| 异步非阻塞 + 回调 | Node.js / Netty | 复杂(回调地狱) | 好 |
| 异步非阻塞 + async/await | Rust tokio | 较好 | 极好 |
| 同步写法 + 异步执行 | Go | 最简单 | 好 |
Go 的方案本质上和 Reactor 模型一样高效,但开发者不需要处理回调、Future、async/await 等复杂概念。
常见面试问题
Q1: Go 用的是 epoll 的 ET 还是 LT 模式?
答案:Go 使用 ET(Edge Triggered)边缘触发模式。这意味着每个事件只通知一次,Go runtime 需要一次性读/写完所有数据。ET 模式减少了 epoll_wait 的系统调用次数,性能更好。
Q2: goroutine 阻塞在网络 IO 时线程会怎样?
答案:goroutine 调用 gopark 被挂起,让出 M(OS 线程)给其他 goroutine 使用。当数据就绪时,goroutine 被放回调度队列等待运行。整个过程中 OS 线程不会被浪费。