应监听 request.context().done() 通道并周期性轮询 ctx.err(),其中 context.canceled 表示客户端断开,context.deadlineexceeded 表示服务端超时;sse 等流式响应需每次写入前检查 ctx.err() 并及时 flush。

如何判断 http.Request 对应的客户端已断开连接
Gin 本身不提供直接的“连接关闭检测 API”,它依赖底层 net/http 的机制。核心在于:客户端断开后,ResponseWriter 写入会失败,但更早、更主动的判断方式是监听 Request.Context().Done() 通道 —— 这是标准 Go HTTP 服务对连接中断/超时的统一信号。
常见错误是试图在 handler 开头就检查 ctx.Err(),但此时往往还没触发;真正可靠的做法是在写响应前、或长耗时操作中周期性轮询:
-
select监听ctx.Done()+time.After()(适合流式响应或等待外部事件) - 对大文件下载、SSE 或长时间轮询,每次
c.Writer.Write()后检查c.Writer.Hijacked() == false && c.Writer.Size() == 0并结合ctx.Err()判断是否已断开 - 避免仅靠
write系统调用返回broken pipe错误来判断 —— 此时响应已部分发出,且错误发生在写入阶段,不够及时
为什么 context.DeadlineExceeded 不等于客户端断开
ctx.Err() 返回 context.DeadlineExceeded 表示请求整体超时(由 Gin 的 gin.Timeout() 中间件或 http.Server.ReadTimeout 触发),而 context.Canceled 才代表客户端主动关闭连接(如用户关浏览器、curl 被 Ctrl+C、移动端切后台)。两者需区分处理:
- 收到
context.Canceled:可立即终止业务逻辑,无需再写响应 - 收到
context.DeadlineExceeded:可能客户端还在,只是慢;若已开始写响应,仍应尽量完成(除非业务不允许) - Gin 默认不设置
Context超时,context.Canceled是唯一能反映真实断连的信号
在流式响应(如 SSE)中检测断连的实操要点
SSE 场景下客户端断开最隐蔽,必须主动探测。Gin 的 c.Stream() 或直接操作 c.Writer 时,关键点如下:
- 每次向客户端写入数据前,先执行
if ctx.Err() != nil { return } - 写完一行(如
data: ...\n\n)后调用c.Writer.Flush(),否则数据滞留在缓冲区,无法触发底层 write 失败 - 不要依赖
io.WriteString(c.Writer, ...)的返回值判断断连 —— 它可能成功返回,但内核 socket 已关闭,下次 flush 才暴露问题 - 示例片段:
for range ticker.C { select { case
net/http 底层的连接关闭信号为何不可靠
Go 的 net/http 服务器在客户端 TCP FIN 到达时,并不会立刻通知 handler;而是等到你尝试写响应时才返回 write: broken pipe 或 write: connection reset by peer。这意味着:
- 如果 handler 只读不写(如纯查询接口),永远收不到断连信号,
ctx.Done()是唯一途径 - 即使写了响应,错误也可能延迟到下一次
Write()或Flush(),而非首次写入 - HTTP/2 场景下更复杂:单个 TCP 连接复用多请求,
ctx.Cancel仍有效,但底层错误类型可能变为http2: stream closed - 因此,所有需要感知断连的逻辑,必须基于
ctx.Done(),而不是等待 I/O 错误
真正难处理的是那些没显式使用 context 的旧代码,或者把 context 当作超时工具而忽略其取消语义 —— 这类地方最容易漏掉断连判断。











