io.EOF 表示对端正常关闭,net.ErrClosed 表示本地已关闭,syscall.ECONNRESET 等才是网络异常中断的真实错误;应使用 errors.Is/errors.As 精准判断,而非字符串匹配。

io.EOF 是对端关闭连接的明确信号,net.ErrClosed 表示本地已调用 conn.Close(),而 syscall.ECONNRESET、syscall.ETIMEDOUT 等底层错误码才是网络异常中断(如断网、防火墙拦截、服务崩溃)的真实指纹——靠字符串匹配 "connection reset" 或 "use of closed" 会漏判、误判,且无法跨平台兼容。
读写时如何准确识别连接断开
Go 的网络错误不是“字符串日志”,而是可类型断言的结构化值。关键不是看错误文本,而是用 errors.Is 和 errors.As 做精准分类:
-
io.EOF:读操作返回此值,说明对端已关闭写方向(如调用了close()),属于**正常断连**,应退出读循环并清理资源 -
errors.Is(err, net.ErrClosed):写操作返回此值,代表本地连接已被显式关闭,不能再读写 -
*net.OpError:绝大多数异常错误都包装在此类型中。需先errors.As(err, &netErr)提取,再判断:-
netErr.Timeout()→ 超时(含context.DeadlineExceeded) -
netErr.Temporary()→ 临时性错误(如syscall.EAGAIN),可重试 -
errors.Is(netErr.Err, syscall.ECONNRESET)→ 连接被对端强制重置(常见于进程 crash 或中间设备切断) -
errors.Is(netErr.Err, syscall.ENETUNREACH)→ 网络不可达(路由失效、网卡 down)
-
不发数据怎么知道连接还活着?
TCP 层的 KeepAlive 默认间隔长(Linux 通常 2 小时),无法满足应用层快速感知需求。必须在应用层主动探测:
- 对 MySQL:使用
go-sql-driver/mysql的CheckConnLiveness = true配置,它会在复用连接前调用底层syscall.Read检查套接字状态,遇到syscall.EAGAIN视为正常,io.EOF或其他错误则标记连接失效 - 对 WebSocket:用
gorilla/websocket的SetPongHandler更新最后活跃时间,并启动 goroutine 定期调用WriteControl(websocket.PingMessage, ...);若WriteControl失败或ReadMessage返回websocket.IsUnexpectedCloseError,即判定断开 - 通用 TCP 连接:可封装一个非阻塞探测函数,类似 MySQL 驱动的
connCheck,通过rawConn.Read(...)尝试读 1 字节,根据n == 0 && err == nil(EOF)、err == syscall.EAGAIN(正常空闲)、err != nil(异常)三态判断
为什么 net.Dial 成功不代表连接可用?
net.Dial("tcp", addr) 只完成三次握手,返回 nil 错误仅表示连接建立成功,**不保证后续读写一定通**。真实场景中常见问题:
- 连接建立后,中间 NAT/防火墙在空闲超时后单向丢弃包,但 TCP 状态仍显示 ESTABLISHED
- 服务端进程崩溃,但 FIN 包未发出,客户端
Read会一直阻塞(除非设了 deadline) - 云环境 LB 主动踢掉空闲连接,客户端无感知
因此,任何长连接都必须配合:读写 deadline + 心跳探测 + 错误分类处理。例如设置 conn.SetReadDeadline(time.Now().Add(30 * time.Second)),并在每次 Read 后检查是否是 net.Error 且 Timeout() == true。
容易踩的坑:跨平台与兼容性盲区
不是所有系统都支持底层 socket 探测:
-
connCheck在 Linux/macOS 可用syscall.Read,但在 Windows 或某些容器环境(如部分 musl libc 镜像)会 fallback 到空实现(直接返回nil),导致心跳失效 -
syscall.ECONNRESET在 Windows 上对应的是wsa.WSAECONNRESET,不能直接用errors.Is(err, syscall.ECONNRESET)判断,需用x/sys/windows中的错误码或统一走netErr.Timeout()/netErr.Temporary()分支 - MySQL 驱动的
ReadTimeout配置只影响connCheck探测阶段,不影响实际查询;若没设,探测可能永久阻塞
最稳妥的做法是:始终设 deadline;优先用 errors.Is 判断标准错误变量;对非标准 syscall 错误,结合 netErr.Op("read"/"write")和 Temporary() 做兜底策略。别信连接“看起来还开着”。










