Go 的 net.Conn 需应用层心跳保活,仅设 SetKeepAlive 不足;应结合 time.Ticker 每 20 秒发 PING/PONG,并用 select+超时防阻塞。

Go 的 net.Conn 默认不自动发心跳,得自己加
Go 标准库的 net.Conn 对象本身没有内置心跳机制,TCP 层的 KeepAlive 仅触发底层探测(OS 级),不能保证应用层感知对端存活,更无法应对中间设备(如 NAT、防火墙)静默丢包。真要保活,必须在应用层定时发业务心跳包。
常见错误是只设了 SetKeepAlive(true) 就以为万事大吉,结果连接在 5 分钟后被网关断开却毫无察觉。
- 启用系统级 keepalive:
conn.SetKeepAlive(true)+conn.SetKeepAlivePeriod(30 * time.Second)(Go 1.19+ 支持,旧版需用syscall设置) - 但必须配合应用层心跳:比如每 20 秒发一个
"PING",对方回"PONG" - 注意:
SetKeepAlivePeriod最小值受 OS 限制(Linux 通常 ≥ 1s,实际生效可能四舍五入到秒)
用 time.Ticker 发心跳比 time.AfterFunc 更稳
time.AfterFunc 每次重置定时器容易漏发——比如上一次心跳 write 阻塞了 3 秒,下一次就晚于预期;而 time.Ticker 是固定节奏驱动,更适合保活这种强周期性任务。
典型场景:长连接 WebSocket 或自定义 TCP 协议服务,客户端需持续上报在线状态。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 别这么写(易 drift):
time.AfterFunc(20*time.Second, sendPing),然后在sendPing末尾再调一次 - 应该用:
ticker := time.NewTicker(20 * time.Second),在for range ticker.C里发包 - 务必 select 套一层超时控制,避免 write 阻塞整个 ticker:
select { case
Write 失败不等于连接断了,得结合 Read 和 Deadline 判断
只监听 Write 错误会误判:比如对方接收缓冲区满、临时网络抖动,Write 返回 io.ErrShortWrite 或临时 syscall.EAGAIN,但连接其实还活着。真正断连往往先体现在 Read 返回 io.EOF 或超时。
关键点:心跳逻辑里不能只看发包成不成功,要双向验证。
- 给 conn 设读写 deadline:
conn.SetReadDeadline(time.Now().Add(25 * time.Second))(略大于心跳间隔) - 发完
PING后立刻Read期待PONG,若超时或返回io.EOF/net.OpError,才认为连接失效 - 注意:
SetDeadline是绝对时间,每次 read/write 前都得重设,别设一次就不管了
心跳包内容尽量轻,别带 JSON 或换行符
简单协议最可靠。见过有人用 json.Marshal(map[string]string{"type":"ping","ts":time.Now().String()}) 发心跳,结果因为时间字符串长度不定、含空格/冒号,被中间代理当成非法协议直接断连。
真实部署中,NAT 设备、4G 路由器、某些企业防火墙会对“非标准”流量做深度检测或限速。
- 推荐格式:
[]byte("PING\n")或固定 4 字节:[4]byte{0x01, 0x00, 0x00, 0x00} - 响应也固定:
"PONG\n",不用解析,bytes.Equal(buf[:4], []byte("PONG"))就行 - 千万别在心跳里塞
uuid、时间戳、版本号等可变字段——增加解析负担且无实际意义
心跳不是设了就高枕无忧,最麻烦的是边界情况:比如对方进程卡死但 TCP 连接没关闭,或者心跳响应延迟刚好卡在 deadline 边缘。这些地方得靠日志打点+连接状态机来兜底,光靠定时器和超时不够。

















