Go net.Conn空闲连接不释放导致内存上涨,需显式控制生命周期:HTTP场景用http.Server的IdleTimeout等超时参数;裸TCP需自实现心跳+定时器Reset;并用pprof和trace定位残留goroutine与未执行Close的位置。

Go net.Conn 空闲连接不释放,内存持续上涨
Go 的 net.Conn 本身不自动管理空闲生命周期,只要没调用 Close(),底层文件描述符和关联的 runtime goroutine、buffer、TLS 状态就一直占着内存。十万级连接若长期 idle(比如 HTTP/1.1 keep-alive 未设超时、TCP 心跳缺失、客户端异常断连但服务端未感知),runtime.MemStats.Alloc 会明显爬升,pprof 查看常发现大量 net.(*conn).readLoop 和 bufio.NewReaderSize 占用堆。
这不是 Go bug,是设计使然:连接生命周期必须由业务显式控制。
用 net/http.Server 的 IdleTimeout + ReadHeaderTimeout 强制驱逐
HTTP 场景下最直接有效的方式是启用服务器级空闲控制,避免手动遍历连接——Go 1.8+ 已内置支持,无需第三方库。
-
ReadTimeout防止请求头读取卡死(如客户端只发一半 header) -
ReadHeaderTimeout更精准,仅约束 header 解析阶段(推荐设为 5–10s) -
IdleTimeout是关键:从上一个 request 完结到下一个 request 开始的等待上限(推荐设为 30–60s) -
WriteTimeout可选,防止 response 写入阻塞太久
示例:
立即学习“go语言免费学习笔记(深入)”;
srv := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadHeaderTimeout: 8 * time.Second,
IdleTimeout: 45 * time.Second,
WriteTimeout: 30 * time.Second,
}
log.Fatal(srv.ListenAndServe())注意:IdleTimeout 对非 HTTP 连接(如裸 TCP、WebSocket)无效,它依赖 http.conn 的内部状态机。
裸 TCP 或自定义协议必须自己实现连接心跳与回收
没有 HTTP 协议栈帮你 track 每个连接的 activity 时间点,就得在 net.Conn 上叠加心跳逻辑。核心思路是:每个连接配一个带超时的 time.Timer,每次读/写都 Reset(),超时则 Close()。
- 别用
time.AfterFunc:无法取消,易泄漏 timer 对象 - 避免在
Read()循环里反复timer.Reset()而不Stop()原 timer,否则旧 timer 仍可能触发 - 超时关闭前,先
conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))确保Read()尽快返回,再Close() - 务必 recover 读 goroutine panic(如已关闭的 conn 被并发读),否则 goroutine 泄漏比连接还严重
简化骨架:
func handleConn(conn net.Conn) {
defer conn.Close()
timer := time.NewTimer(60 * time.Second)
defer timer.Stop()
<pre class="brush:php;toolbar:false;">for {
select {
case <-timer.C:
conn.Close() // 触发 read 返回 io.EOF
return
default:
}
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, err := conn.Read(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue // 重试读,不重置 timer
}
return // real error or closed
}
timer.Reset(60 * time.Second) // 活跃,续期
// ... 处理数据
}}
用 pprof + go tool trace 快速定位残留连接来源
清理后内存不降?大概率有 goroutine 还在 hold 连接引用,或 Close() 调用被忽略/吞掉(比如 defer 放错位置、recover 吞了 panic)。
- 启动时加
_ "net/http/pprof",访问/debug/pprof/goroutine?debug=2查看所有 goroutine 栈,搜readLoop、serve、handleConn - 跑
go tool trace录制 30 秒,打开后重点看 “Goroutines” 视图,筛选长时间运行(>10s)且状态为running或syscall的 G,点进去看它 block 在哪行 - 检查是否在某个 channel receive 或 mutex lock 上卡住,导致连接无法走到
Close() - 确认
Close()是否真被执行:加日志或断点,尤其注意 defer 是否在错误分支外提前 return
真正难的不是关连接,而是确保每个连接路径都有且仅有一次 clean close —— 特别是带重连、代理、TLS handshake 中断等复杂场景。


















