直接调用 syscall.read/write 会阻塞 os 线程(m),而 *os.file 和 net.conn 已自动对接 runtime netpoller,实现 goroutine 挂起、m 继续调度;仅当实现零拷贝协议栈、对接硬件驱动或极致性能优化时才需裸写非阻塞 syscall。

直接调用 syscall.Read 或 syscall.Write 会导致整个 OS 线程(M)挂起,阻塞所有绑定在该 M 上的 goroutine——这不是并发,是假并发。标准库的 *os.File 和 net.Conn 已自动对接 runtime 的 netpoller,你几乎不需要裸写非阻塞 syscall。
什么时候真得自己包装非阻塞 syscall?
仅当满足全部三个条件时才考虑:实现零拷贝协议栈、对接硬件驱动、或绕过标准库做极致性能优化。普通 IO、日志、配置读写、网络代理等场景都不在此列。
- fd 必须已设为非阻塞模式:
unix.SetNonblock(fd, true)(Unix)或创建时带FILE_FLAG_OVERLAPPED(Windows) - 必须显式调用
runtime.Entersyscall()和runtime.Exitsyscall(),否则调度器仍按阻塞逻辑处理,P 会被长期霸占 - 错误码判断不能只看
syscall.EAGAIN:Unix 下还要兼容syscall.EWOULDBLOCK;Windows 下需区分WSA_IO_PENDING(异步进行中)和真实错误
为什么 os.File.Read 不卡 M,而 syscall.Read 会?
因为 *os.File 的 Read() 方法内部触发的是封装好的非阻塞路径:它把 fd 注册进 netpoller,goroutine 在等待时被挂起(G 状态变为 Gwaiting),M 则立即返回调度队列继续跑其他 G。而 syscall.Read 是直通内核的裸调用,运行时无法感知其就绪时机,只能让 M 进入休眠。
- Unix 下:标准库用
epoll_ctl(EPOLL_CTL_ADD)注册 fd,epoll_wait统一等待事件 - Windows 下:用 IOCP(I/O Completion Ports)配合
WSARecv或ReadFileEx - 关键点:这些机制都由 runtime 自动管理,你只需用
os.OpenFile或net.Dial,不要自己syscall.Open
从 C 传入的 fd 怎么安全复用?
可用 os.NewFile(uintptr(fd), "name") 包装成 *os.File,再调 Read() —— 但 Windows 下此方式不触发异步 IO,仍可能阻塞 M。
- Unix/Linux/macOS:可行,fd 若已设
O_NONBLOCK,包装后能走 netpoller - Windows:不可行,
os.NewFile无法将 HANDLE 关联到 IOCP,必须用golang.org/x/sys/windows手动调ReadFileEx+OVERLAPPED - 更稳妥做法:避免接收裸 fd,改用 socketpair / pipe / domain socket 等 Go 可控通道传递数据
真正难的不是“怎么写非阻塞 syscall”,而是判断“是否真的需要它”。绝大多数时候,你只是没意识到 os.File 和 net.Conn 的 Read/Write 本来就是协作式非阻塞的——它们返回时,G 挂起了,M 活着,P 在跑别的任务。裸 syscall 是最后一道门,推开前请确认门后没有更简单的路。











