Go 的 IO 多路复用
Go 的 IO 多路复用、epoll vs select 差异,以及 Go runtime 中是如何使用它们的。
✅ 一、什么是 IO 多路复用?
IO 多路复用解决的问题是:
一个线程同时监听多个文件描述符(socket 等)的 IO 状态变化
不需要为每个连接创建一个线程或阻塞在一个 IO 上
常见系统调用:
- select
- poll
- epoll(Linux)
- kqueue(BSD / macOS)
- IOCP(Windows)
✅ 二、select vs epoll 区别(重点)
| 对比项 | select | epoll | 
|---|---|---|
| 支持的 fd 数量 | 有上限,一般1024 | 理论无限(由系统内存决定) | 
| 时间复杂度 | O(n) 每次扫描全部 fd | O(1) 回调通知,只处理活跃 fd | 
| 内核与用户态交互 | 每次都复制整个 fd 集合 | 使用内核维护红黑树和就绪链表,无需重复传输大量数据 | 
| 触发模式 | 只支持 水平触发 | 支持 水平触发 + 边缘触发(更高效) | 
| 资源消耗 | 大,重复构造 fd 集合 | 小,只在 add/del 时修改 | 
| 大并发性能 | 差 | 优秀,适合 10w+ 连接 | 
一句总结:
select:每次全部扫描
epoll:事件通知,只处理变化的 FD
⇒ 大并发下 epoll 优势极大
✅ 三、为什么 Go 用 epoll(Linux)?
Go 的网络模型是 goroutine + epoll/kqueue + 协程调度器:
用户写代码:阻塞 I/O
↓
Go runtime:用 epoll/kqueue 监控 socket
↓
有事件就唤醒对应的 goroutine
实际上,netpoller 负责底层事件循环:
| 平台 | 多路复用机制 | 
|---|---|
| Linux | epoll | 
| macOS / FreeBSD | kqueue | 
| Windows | IOCP | 
你写的看似阻塞的 conn.Read() 实际是:
- goroutine 被 park(挂起)
- IO 可读时,epoll 通知 runtime
- runtime 唤醒该 goroutine
Goroutine ≠系统线程
一个线程同时调度成千上万 goroutines
✅ 所以 Go 在高并发网络服务中天生强大。
✅ 四、代码层使用体验
你写的代码不需要关心 epoll:
for {n, err := conn.Read(buf)if err != nil {return}fmt.Printf("read %d bytes\n", n)
}
Go runtime 自动协程化。
✅ 五、什么时候 select 仍会被使用?
- 小规模 fd 管理,简单快速
- 跨平台兼容
- Go 内部在 netpoll未启用场景下 fallback
✅ 六、一句话总结
select 采用轮询,fd 数量有限且 O(n) 性能差;
epoll 采用事件回调,仅返回活跃 fd,支持更大并发;
Go 的 Runtime 在 Linux 上使用 epoll 实现 IO 多路复用,协程调度隐藏了异步 IO 的复杂性,使阻塞 API 实际是高效的事件驱动模型。
实际编码中为什么感受不到?
理解 Go 网络模型的核心:
Go 代码里看不到
select/epoll,但它们的特征仍然“渗透”在你的代码行为里。
我们从 开发者视角 和 底层表现 两个层面看。
✅ 一、Go 层看不到 epoll/select,但它们在底层驱动
代码可能长这样:
for {conn, err := listener.Accept()if err != nil {continue}go handle(conn)
}func handle(conn net.Conn) {buf := make([]byte, 1024)for {n, err := conn.Read(buf)if err != nil {return}conn.Write(buf[:n])}
}
看起来是同步阻塞式写法:
- Accept()阻塞
- Read()阻塞
- Write()阻塞
👉 但在 Go runtime 底层:
- 每个 goroutine 被 runtime 管理;
- netpoll使用 epoll(Linux) / kqueue(macOS) / IOCP(Windows);
- 当 IO 不可读/写时,goroutine 被“挂起”(park);
- 事件到达后,runtime 唤醒对应 goroutine。
所以:
阻塞 API,非阻塞执行。
这就是 Go 的“同步写法 + 异步模型”特征。
