net/http 默认 servemux 不支持非阻塞多路复用,因其为阻塞式调度器,goroutine 会全程占用直至 handler 返回,无法挂起等待异步事件;实现非阻塞需手动接管 net.conn 生命周期,配合 channel、context 和 http/2 协议栈,或极少数场景下直用 syscall 轮询。

为什么不能直接用 net/http 默认 ServeMux 做非阻塞多路复用
默认的 http.ServeMux 是阻塞式调度器:每个请求进来的 goroutine 会一直占着直到 handler 返回,没法在中间暂停、让出控制权给其他请求。它不支持挂起一个连接等待某个异步事件(比如 WebSocket 消息、定时器、外部 RPC 返回),也就谈不上“非阻塞多路复用”。真要实现这个,得绕开标准 mux,自己接管连接生命周期。
用 net.Conn + runtime.Gosched() 或 channel 控制权移交
核心思路是:不依赖 http.Server 的自动 accept→serve 流程,而是手动调用 listener.Accept(),拿到 net.Conn 后立刻丢进 goroutine 处理;在 handler 内部用 channel 等待事件,而不是同步阻塞调用。关键点在于——别让 goroutine 卡死在 syscall 上。
-
http.ReadRequest()和resp.Write()本身是阻塞的,但它们只操作已建立的Conn,不影响其他连接 - 真正需要非阻塞的是“等待下游响应”这类逻辑,比如调用
grpc.ClientConn.Invoke()或读redis.Conn,这些必须用select+context.WithTimeout()包裹 - 避免在 handler 里写
time.Sleep()或无缓冲 channel 发送——这会卡住整个 goroutine
用 golang.org/x/net/http2 配合自定义 Handler 实现 HTTP/2 多路复用
HTTP/2 本身支持单连接多 stream,并发由协议栈管理。Go 标准库的 http2.Server 在启用后会自动把多个请求复用到同一个 net.Conn 上,但前提是你的 handler 不主动关闭连接或写死 response body。
- 必须显式启用 HTTP/2:
http2.ConfigureServer(server, &http2.Server{}) - handler 中不要调用
resp.(http.Flusher).Flush()频繁刷缓冲——这会干扰 stream 流控 - 如果返回
io.Reader(比如文件流),确保底层 reader 支持io.ReaderAt或能被http.ServeContent安全包装,否则可能触发阻塞读 - 错误日志里出现
http2: stream closed,大概率是 handler panic 或提前 close了 response writer
真正非阻塞:用 epoll/kqueue + syscall.ReadNonblock() 自己轮询(极少数场景)
只有当你需要绕过 Go runtime 网络栈、直通系统调用时才考虑这条路,比如写高性能代理或协议网关。Go 的 net 包默认使用 blocking socket + goroutine per connection,已经足够高效;强行切到 nonblocking + event loop 反而容易因 goroutine 调度延迟引入抖动。
- 要用
syscall.Syscall()或golang.org/x/sys/unix手动管理 fd 和事件循环 -
net.Conn接口无法复用,你得自己封装 read/write buffer、解析 HTTP header、维护 connection state - Go 1.22+ 的
runtime_pollWait底层其实也是 epoll,但抽象层做了大量优化,自己重写几乎没收益
多数业务服务根本不需要走到这一步——先确认瓶颈真在连接调度,而不是 DB 查询慢或 JSON 解析卡住 CPU。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











