conn.Read() 返回 0 字节且 err 为 io.EOF 时必须立即关闭连接,否则会导致空转和 CPU 暴涨;需同时检查 n == 0 和 err == io.EOF 作为终止信号,而非仅判断 err != nil。

conn.Read() 返回 0 字节时必须关闭连接
这是最容易被忽视却最致命的坑:当 conn.Read() 返回 read_len == 0 且错误为 io.EOF(或有时无错误),说明对端已发送 FIN,连接进入半关闭状态。此时若继续循环调用 Read(),会立即返回 0 + nil,造成空转、CPU 暴涨。
- ❌ 错误做法:只检查
err != nil就 break,却忽略read_len == 0的语义 - ✅ 正确做法:把
read_len == 0和err == io.EOF一起视为连接终止信号,立刻conn.Close() - ⚠️ 注意:
io.EOF不是异常,是正常关闭流程的一部分;而"broken pipe"或"connection reset by peer"才是异常断连,也需关闭
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 || err == io.EOF {
log.Println("对端关闭连接")
return
}
if err != nil {
log.Printf("读取失败: %v", err)
return
}
// 处理 buf[:n]
}
}
设置读写超时 + 心跳探测防中间设备断连
NAT、防火墙、负载均衡器常在空闲 30–300 秒后静默 kill 连接,而 Go 程序毫无感知,后续写入直接 panic 或阻塞。仅靠 io.EOF 检测远远不够。
-
conn.SetReadDeadline()和conn.SetWriteDeadline()必须显式设置,不能依赖系统默认 - 服务端建议用心跳(如每 25 秒发一次 ping)+ 超时(如 35 秒未收 pong 就关连接)组合策略
- 客户端重连时,不要立即重试,应指数退避(
reconnectionDelay: 1000,reconnectionDelayMax: 5000)
超时值要小于中间设备的 idle timeout,否则永远等不到断开信号。
并发场景下避免重复 Close 和 goroutine 竞态
多个 goroutine 共享一个 *net.TCPConn 时,谁该关?什么时候关?不加协调极易 panic(close of closed channel 类似逻辑也适用于连接)。
- 用
sync.Once包裹conn.Close(),确保只执行一次 - 更推荐:用
context.Context控制生命周期,读/写 goroutine 都监听ctx.Done(),收到信号后主动退出并触发关闭 - 禁止在 defer 中无条件
conn.Close()—— 如果连接已被其他 goroutine 关闭,defer 会 panic
var once sync.Once
func safeClose(conn net.Conn) {
once.Do(func() {
conn.Close()
})
}
服务整体退出时:先停 Listener,再等连接 Drain
程序收到 SIGINT 或 SIGTERM 后,不能直接 os.Exit()。要分两步:停止接受新连接,再等待已有连接自然结束(或超时强制终止)。
- 调用
listener.Close()会让阻塞的Accept()立即返回错误(如"use of closed network connection"),主循环可快速退出 - 每个活跃连接应注册到
sync.WaitGroup,Accept()后wg.Add(1),处理完wg.Done() - 主 goroutine 在关闭 listener 后,用
wg.Wait()等待所有连接处理完毕,再退出
如果某些连接卡死(比如客户端不读响应),必须设超时(如 10 秒),否则整个 shutdown 会被拖住。










