Go原生net包默认是同步阻塞模型,其net.Listener.Accept()和conn.Read()/Write()均依赖阻塞式系统调用,未注册fd到epoll/kqueue,也不支持事件驱动回调;真Reactor需手动操作syscall.epoll系列函数,设fd为非阻塞并自主管理事件循环与fd生命周期。

Go 原生 net 包默认是同步阻塞模型,直接用 conn.Read() 或 conn.Write() 会挂住 goroutine;要实现真正非阻塞的 Reactor 模式,必须绕开默认行为,手动接入系统级 I/O 多路复用(如 epoll / kqueue),并自己管理事件循环和回调调度。
为什么 net.Listen() 默认不是 Reactor?
Go 的 net.Listener.Accept() 表面看是“等待连接”,实际底层调用了阻塞式 accept() 系统调用(Linux 下),哪怕你用 runtime.Gosched() 或 select{} 也无法让它变成事件驱动。它不注册 fd 到 epoll,也不通知你“有新连接就绪”,只是傻等。
- 每个
conn对象默认启用阻塞 I/O,Read()调用会一直卡在内核态,直到数据到达或超时 -
net/http.Server看似并发高,本质是为每个连接起一个 goroutine——这是“多线程阻塞模型”的 Go 版,不是 Reactor - 真 Reactor 要求:单个 goroutine(或少量)轮询所有活跃 fd,只在可读/可写时才触发回调,绝不阻塞
如何用 syscall + epoll 实现最小 Reactor 循环?
核心是放弃 net.Conn 封装,直接操作文件描述符(fd),用 syscall.EpollCreate1()、syscall.EpollCtl() 和 syscall.EpollWait() 构建事件循环。Go 标准库不暴露 epoll 接口,所以得用 syscall 或 cgo 封装。
- 监听 socket 必须设为非阻塞:
syscall.SetNonblock(lfd, true),否则Accept()仍会阻塞 - 每个新连接返回的
conn.FD()(需反射或 unsafe 获取)也要设非阻塞,否则Read()仍卡住 -
epoll_wait返回的是就绪 fd 列表,你要自己查哪个 fd 触发了什么事件(EPOLLIN/EPOLLOUT),再分发给对应回调函数 - 回调里不能做耗时操作(比如解析 JSON、查 DB),否则阻塞整个事件循环;必须交给 worker goroutine 异步处理
示例片段(简化):
使用 @ainative/react-sdk 为 React 应用添加 AI 聊天和积分。适用于 (1) 安装 @ainative/react-sdk,(2) 使用 useChat hook 实现聊天完成。
epollFd := syscall.EpollCreate1(0)
syscall.EpollCtl(epollFd, syscall.EPOLL_CTL_ADD, lfd, &syscall.EpollEvent{Events: syscall.EPOLLIN, Fd: lfd})
for {
events := make([]syscall.EpollEvent, 128)
n := syscall.EpollWait(epollFd, events[:], -1) // -1 表示永久阻塞等待
for i := 0; i < n; i++ {
if events[i].Fd == lfd {
// accept 新连接,设非阻塞,add 到 epoll
} else {
// 调用该 fd 绑定的 onRead 回调
handlers[events[i].Fd].onRead()
}
}
}
netpoll 与 runtime.netpoll 的区别在哪?
Go 运行时内部确实有个 netpoll(位于 src/runtime/netpoll.go),但它不是公开 API,且只服务于 Go 自身 goroutine 调度:当某个 goroutine 在 read 上阻塞时,runtime 把它 park 住,并把 fd 注册到 netpoll;一旦 fd 就绪,唤醒 goroutine。这仍是“隐藏的阻塞 + 调度器接管”,不是用户可控的 Reactor。
-
runtime.netpoll不暴露 fd 控制权,你无法主动epoll_ctl添加自定义 fd - 它和用户代码隔离,无法插入自定义回调,也不能绕过
net.Conn的阻塞封装 - 第三方库如
gnet、evio是自己实现 epoll/kqueue 封装,完全脱离 runtime.netpoll,这才是真 Reactor
容易踩的坑:回调里 panic、fd 泄露、边缘事件没处理
Reactor 模式下,事件循环是单点故障,一个未捕获的 panic 会让整个服务停摆;而 fd 管理全靠手动,漏掉 close() 或重复 epoll_ctl(ADD) 会导致资源耗尽。
- 所有回调必须包一层
defer func(){if r:=recover();r!=nil{log.Printf("panic: %v", r)}}() - 每次
accept()后拿到新 fd,必须立即epoll_ctl(ADD),否则永远收不到后续读事件 -
EPOLLHUP和EPOLLERR事件常被忽略,但它们代表对端关闭或错误,必须close(fd)并从 epoll 移除 - 写事件(
EPOLLOUT)只在缓冲区有空闲时触发,但你不能假设一次Write()就写完全部数据;要维护 write buffer,仅在EPOLLOUT时尝试 flush
真正的 Reactor 不是加个 select 或开一堆 goroutine 就算,它要求你直面系统调用、精确控制 fd 生命周期、并把业务逻辑切成可中断的小块交给回调——这些细节错一点,性能就归零,稳定性就崩盘。


















