Go 的 netpoller 是运行时自动管理的 I/O 多路复用封装,开发者不可直接调用 epoll 等系统调用;它绑定每个 M、与 G-P-M 调度深度集成,所有标准库网络操作均透明使用该机制。

Go 的 netpoller 不是用户直接调用 epoll
Go 网络库底层确实用到了 epoll(Linux)、kqueue(macOS)或 iocp(Windows),但你写代码时完全接触不到它。Go 运行时把 I/O 多路复用封装进 netpoller,作为 runtime 和 net 包之间的胶水,对开发者透明。你没法手动初始化一个 epoll_fd,也没法调用 epoll_ctl——这些全由 runtime.netpoll 在后台自动调度。
常见错误现象:go tool trace 里看到大量 netpoll 相关事件,误以为自己在“用 epoll”;或者试图用 syscall.EpollCreate1 混合使用,结果触发 goroutine 调度紊乱甚至死锁。
- 所有标准库网络操作(
net.Conn.Read、http.Serve、net.Listen)都走这套机制,无需额外配置 -
netpoller绑定的是每个 M(OS 线程)上的一个轮询器,不是全局单例;goroutine 阻塞在 socket 上时,会被挂起并交还 P,不占用线程 - 如果你强行绕过
net包,用syscall直接操作 fd 并设为非阻塞,又没配好runtime.Entersyscall/runtime.Exitsyscall,会卡住调度器
想观察 netpoller 行为,得看 runtime trace 和 goroutine stack
netpoller 本身没有公开 API,但它的调度痕迹能通过运行时工具捕获。关键不是“怎么启用”,而是“怎么验证它正在工作”。
使用场景:排查高并发下连接堆积、goroutine 泄漏、或 read/write timeout 不生效的问题。
立即学习“go语言免费学习笔记(深入)”;
- 启动程序时加
GODEBUG=netdns=cgo+1没用,真正相关的是go tool trace:运行go run -gcflags="-l" main.go & GODEBUG=netpolldebug=1(仅调试版 Go 支持) - 更可靠的方式是采集 trace:
go tool trace -http=localhost:8080 ./myserver,然后在浏览器打开,重点关注Network poller和Goroutines时间轴 - 查看阻塞点:
kill -SIGQUIT <pid>打印堆栈,如果大量 goroutine 停在runtime.netpoll或internal/poll.runtime_pollWait,说明 netpoller 正常接管了 I/O
自定义 epoll 场景极少,且代价远高于收益
真需要手撸 epoll,通常只出现在两类地方:极低延迟定制协议栈(如高频交易网关)、或对接已有 C/C++ 网络框架(比如 DPDK + epoll 混合)。普通 HTTP/GRPC/TCP 服务完全没必要。
性能影响很实际:Go 的 netpoller 是和 G-P-M 调度深度绑定的,手动 epoll 意味着你要自己管理 fd 生命周期、事件循环、goroutine 唤醒逻辑,稍有不慎就破坏 GC 安全或导致 select 卡死。
- 若必须混用,用
runtime.SetFinalizer确保 fd 关闭时清理 epoll 注册项,否则 fd 泄漏 -
syscall.EpollWait返回后,不能直接runtime.Goexit或 panic,需用runtime.NewG+goparkunlock模拟调度挂起,否则破坏 goroutine 状态机 - Go 1.22+ 引入了
runtime/netpoll内部重构,部分符号已移除;依赖私有 API 的代码会在 minor 版本升级中崩
netpoller 的兼容性边界在 syscall 和 file descriptor 类型
netpoller 只支持它认识的 fd:由 net 包创建的 socket、监听器、或显式调用 syscall.RawConn.Control 获取控制权的 fd。其他类型(如管道 pipe、inotify fd、tty)默认不被监控。
错误现象:epoll_wait 返回就绪,但 runtime.netpoll 没唤醒对应 goroutine;或 Read 永远阻塞,即使 fd 实际可读。
- Unix domain socket(
unix://)受支持,但 Windows named pipe 不支持 - 通过
os.NewFile包装的 fd,除非调用fd.SyscallConn()并执行conn.Control注册到 netpoller,否则不会被轮询 - Go 对
O_CLOEXEC的处理较保守:子进程继承 fd 后,父进程关闭该 fd 不会自动从 epoll 中删除,可能引发EBADF
最常被忽略的是:netpoller 不处理信号事件(SIGIO)、不感知文件系统变更、也不替代 inotify。它只管“这个 fd 是否可读/可写/出错”,别的都得你自己补。











