net.conn“假活”因tcp不主动检测对端断连,需结合setkeepaliveperiod()(go 1.19+)与应用层双向心跳(ping/pong+独立读写超时)来识别和回收。

为什么 net.Conn 会“假活”?
Go 的 net.Conn 默认不检测对端是否已断开,只要底层 TCP 连接没收到 FIN/RST,Read() 就会一直阻塞或返回 io.EOF(仅当对端明确关闭时)。但现实中常见:客户端崩溃、NAT 超时、防火墙静默丢包——此时连接在 Go 侧仍显示“可写”,Write() 可能成功(数据进内核发送队列),但对方永远收不到。这类连接就是“假活”,必须靠空闲检测主动发现并回收。
SetDeadline() 和 SetKeepAlive() 到底该用哪个?
两者目的不同,常被混淆:
-
SetDeadline()控制单次 I/O 操作超时(如一次Read()最多等 30 秒),适合请求-响应模型,但无法感知长连接空闲; -
SetKeepAlive()启用 TCP 层保活(SO_KEEPALIVE),由内核周期性发探测包,默认间隔 2 小时(Linux),太长,且探测失败后仍需等待Read()或Write()才报错。
真正实用的是组合:SetKeepAlive(true) + SetKeepAlivePeriod()(Go 1.19+),把探测间隔缩到 30 秒内。但注意:Windows 默认不支持 SetKeepAlivePeriod(),得 fallback 到应用层心跳。
应用层心跳怎么写才不踩坑?
在协议层加 ping/pong 帧(如 WebSocket)或自定义二进制包,关键点是:
- 心跳必须双向:服务端发
ping,客户端必须回pong;只单向发没用,无法确认对端是否在线; - 超时判定要分离读/写:用
SetReadDeadline()约束Read(),每次收到数据或 pong 后重置;同时用独立 goroutine 定期Write()ping,失败则关连接; - 避免并发写冲突:所有写操作(业务数据 + ping)必须经同一 channel 或加锁,否则
Write()panic; - 示例节选(简化):
// 每 25 秒发一次 ping,读超时设为 30 秒
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
go func() {
ticker := time.NewTicker(25 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := writePing(conn); err != nil {
conn.Close()
return
}
}
}()
连接池里怎么安全回收空闲连接?
像 database/sql 那样用连接池时,不能只依赖 MaxIdleTime,因为该参数只管“池中空闲多久”,不管“连接本身是否还通”。正确做法:
- 池配置启用健康检查:
&sql.DB{ConnMaxLifetime: 30 * time.Minute, MaxIdleTime: 5 * time.Minute}; - 在
driver.Conn.Ping()实现里,先尝试发一个轻量 ping 包(非 SQL),再Read()等 pong,超时即认为失效; - 如果用
net/http的http.Transport,设置IdleConnTimeout和MaxIdleConnsPerHost,但务必配合服务端的Keep-Alive: timeout=30头,否则 HTTP/1.1 连接可能被中间代理提前断开而不通知 client。
最易忽略的是:空闲检测逻辑和业务读写共享同一个 net.Conn,任何一方调用 SetDeadline() 都会影响另一方——必须用 sync.Mutex 或 context 控制 deadline 设置时机,或者干脆每个连接配独立的读/写 deadline timer。











