Go的netpoll不是epoll封装,而是与调度器协同的内部机制:Read时gopark,事件就绪时标记goroutine为ready;它通过PollDesc关联goroutine而非fd,不可手动替换为syscall.Epoll。

epoll 本身不是异步 IO,它是同步非阻塞的 I/O 多路复用机制;Go 的网络模型也不是真正意义上的异步 IO,而是基于同步非阻塞 epoll(Linux 下)+ 协程调度的“伪异步”封装。两者根本不在同一抽象层级上——epoll 是系统调用接口,Go 是运行时调度策略。
为什么 epoll_ctl + 非阻塞 socket 仍是同步 IO
调用 epoll_wait 返回就绪 fd 后,你仍需自己调用 read 或 write。这些系统调用在非阻塞模式下可能返回 EAGAIN 或 EWOULDBLOCK,但不会帮你把数据从内核缓冲区拷贝到用户内存——这一步必须由你主动完成,且是同步的。
常见错误现象:
- 误以为
epoll_wait返回就绪 = 数据已读完,直接read(fd, buf, sizeof(buf))而不检查返回值,导致部分数据丢失或阻塞(若 socket 意外设为阻塞) - 在 LT 模式下未一次性读完所有可用数据,导致重复触发事件,引发 busy loop
- ET 模式下没用
while (read(fd, buf, size) > 0)循环读,漏掉后续数据
Go net.Conn 底层怎么用 epoll
Go runtime 在 Linux 上默认使用 epoll(通过 runtime.netpoll)管理所有网络 fd,但对用户完全透明。每个 net.Conn.Read 调用背后不是直接系统调用,而是:
- 协程发起
read,发现 socket 无数据 →gopark当前 goroutine,标记为Gwaiting - 同时,runtime 的网络轮询器(一个或多个线程)持续调用
epoll_wait - 一旦某 fd 就绪,轮询器唤醒对应 goroutine,将其置为
Grunnable - 调度器随后让它继续执行
read系统调用(此时必成功或返回短读)
关键点:Go 并没有绕过 read/write 的同步语义,只是把“等待就绪”这个环节从用户代码里抽出来,交给调度器统一管理。
LT 和 ET 模式在 Go 中是否可见
不可见。Go runtime 内部固定使用 EPOLLET(边缘触发),原因很实际:
- ET 模式减少事件重复通知,降低内核和 runtime 的事件处理开销
- Go 协程调度粒度细,单个连接的读写逻辑天然适合“一次就绪、尽量读完”的模式
- LT 模式需要更保守的事件注册/注销逻辑,在高并发下反而增加复杂度
你无法、也不应通过 syscall.EpollCtl 手动干预 Go 的网络 fd 状态——runtime 已全权接管。试图混用会破坏 goroutine park/unpark 的状态机,导致死锁或 fd 漏处理。
什么时候该绕过 Go net 标准库直接用 epoll
极少数场景下才值得考虑:
- 需要精确控制 socket 选项(如
TCP_FASTOPEN、SO_BUSY_POLL)且标准库未暴露 - 实现自定义协议栈(如用户态 TCP),必须跳过内核协议栈直接收发 raw packet
- 超低延迟要求(微秒级),不能接受 goroutine 切换和调度器间接开销
绝大多数 HTTP / RPC / WebSocket 服务,用 net/http 或 net 包即可。自己封装 epoll 不仅开发成本高,还容易因边缘 case(如半关闭、FIN 重传、TCP urgent data)引入难以调试的 bug。
io_uring 的 IORING_OP_READ)在 Go 1.24+ 才开始实验性支持,目前仍需显式启用且不覆盖全部网络路径。现阶段谈“Go 异步网络”,本质还是高效同步非阻塞 + 协程调度 —— 这个事实被很多文档模糊处理,但理解它,才能避开资源泄漏、goroutine 泄露、fd 未关闭等线上高频问题。


















