IdleTimeout 在每次请求处理完后动态重设读截止时间为 time.Now().Add(s.IdleTimeout),仅控制下一次读请求头的等待时长;超时未发新请求则触发 io.EOF 或 net.ErrTimeout 并关闭连接。

IdleTimeout 是怎么被触发的
IdleTimeout 不是靠定时器轮询检查的,而是依赖 Go net.Conn 的 SetReadDeadline 机制,在每次完成一次请求读取后,动态重置读截止时间。它的“空闲”判断逻辑藏在 http.server.serve 循环里:每当一个请求处理完(conn.serve 返回),服务器会检查是否还有后续请求要复用当前连接;如果有,就继续读;如果没有,就调用 setReadDeadline 设置下一次超时点 —— 这个点就是 time.Now().Add(s.IdleTimeout)。
关键点在于:这个 deadline 只控制「下一次读请求头」的等待时长,不是连接总存活时长。一旦客户端在该时间内发来新请求,deadline 就会被再次刷新;否则,readRequest 在尝试读取请求头时会直接返回 io.EOF 或 net.ErrTimeout,最终触发 conn.close。
为什么设置 IdleTimeout = 0 不等于禁用 Keep-Alive
设置 IdleTimeout: 0 表示不主动设置读截止时间,即让底层 TCP 连接按操作系统或中间设备(如 Nginx、ELB)的默认行为决定生命周期。但 Keep-Alive 本身仍有效 —— 只要客户端持续发请求,连接就能复用。
容易踩的坑:
- 误以为
IdleTimeout: 0能“永久保持连接”,实际上连接仍可能被内核tcp_fin_timeout、负载均衡器(如 AWS ALB 默认 60s 空闲超时)或 NAT 设备悄悄回收 - 没配
ReadHeaderTimeout,当客户端只建连不发数据(慢速攻击),IdleTimeout完全不生效,得靠它兜底 - HTTP/2 场景下
IdleTimeout依然起作用,但 HTTP/2 的 PING 帧会重置空闲计时,所以实际断连行为比 HTTP/1.1 更隐蔽
IdleTimeout 和 ReadTimeout / WriteTimeout 的协作关系
三者互不覆盖,各自守一段生命周期:
-
ReadTimeout:从accept开始计时,覆盖请求头 + 请求体读取全过程;若设得太短,大文件上传直接失败 -
WriteTimeout:从请求头读完、进入Handler.ServeHTTP后开始计时,覆盖整个响应写入(包括WriteHeader和Write);超时会导致连接被强制关闭,上游代理(如 Nginx)常因此返回 502 -
IdleTimeout:仅在连接处于 Keep-Alive 状态、且无新请求抵达时生效;它不干预正在处理的请求,也不影响响应写出中
典型错误配置:IdleTimeout: 30 * time.Second,但前端 CDN 或 LB 设置了 60s 空闲超时 —— 客户端还没断,Go 先关了连接,导致复用失败、TCP RST 包发出,客户端收到 ECONNRESET。
如何验证 IdleTimeout 是否生效
别只看日志或监控,直接抓包最可靠:
- 用
curl -v --http1.1 http://localhost:8080/发起请求,观察响应头是否有Connection: keep-alive - 发送第一个请求后,等待超过设定的
IdleTimeout,再发第二个请求;Wireshark 中应看到第一个连接被 FIN 关闭,第二个请求走新连接 - 在 handler 中加
log.Printf("req from %v, conn local: %v", r.RemoteAddr, c.rwc.LocalAddr()),对比两次请求的 socket 地址是否一致
注意:Go 1.19+ 在 http.Server 启动时会自动启用 KeepAlive TCP 选项(SetKeepAlive(true)),但这只是探测链路是否存活,和 IdleTimeout 的应用层空闲判断完全无关 —— 后者才是你真正能控制连接复用寿命的地方。

















