半关闭(half-close)是主动调用shutdown或closewrite单向关闭写通道,仍可读取数据,属正常关闭流程;半打开(half-open)是一方异常断连而另一方未感知,连接假活于established状态,属故障场景,需心跳或keepalive探测清理。

半关闭(Half-Close)和半打开(Half-Open)不是一回事
很多人一看到 “half” 就默认是同一个问题,但 conn.CloseWrite() 是主动发起的半关闭,属于正常 TCP 关闭流程;而半打开(half-open)是连接异常断裂后一方未感知的状态,属于故障场景。前者是你控制的,后者是你要检测和清理的。混淆这两者会导致:用 CloseWrite() 去“修复”半打开连接,结果连接既没真正断开,也没触发对端感知,反而卡在 ESTABLISHED 状态更久。
用 SetReadDeadline 配合心跳探测半打开连接
SetReadDeadline 本身不探测连接状态,但它能帮你把“无响应”转化为可处理的错误。关键在于:必须每次发心跳前重设 deadline,并且心跳帧必须走同一条连接、带明确超时语义。
- 客户端发
"PING\n"后,立刻调用conn.SetReadDeadline(time.Now().Add(5 * time.Second)) - 等待服务端回
"PONG\n";若超时,直接conn.Close() - 服务端收到
PING必须立即响应PONG,不能排队或延迟处理 - 心跳间隔建议设为 15–30 秒,deadline 设为 3–5 秒,避免与业务长请求冲突
启用 TCP keepalive 但别依赖它做快速断连
系统级 keepalive 是兜底手段,不是主力探测机制。Go 1.19+ 支持 SetKeepAlivePeriod(),但它的生效受操作系统限制极大:
- Linux 默认
/proc/sys/net/ipv4/tcp_keepalive_time=7200,即使 Go 设了 30 秒,内核也可能忽略 - keepalive 探测间隔(
tcp_keepalive_intvl)和重试次数(tcp_keepalive_probes)无法通过 Go 标准库设置 - macOS 和 Windows 的行为不一致,实测从首次 idle 到最终判定断连可能长达 12 分钟
- 正确用法是开启 + 设一个合理值,仅作为心跳失败后的保底:
tcpConn.SetKeepAlive(true)+tcpConn.SetKeepAlivePeriod(30 * time.Second)
半关闭连接(CloseWrite)必须配对读取逻辑
调用 conn.CloseWrite() 后,你是在告诉对端:“我发完了”,但本端仍需继续读取对方可能发送的剩余数据或 FIN 包。常见错误是写完就 close 整个连接,导致对端发送的最后几个字节丢失。
- 服务端调用
conn.CloseWrite()后,应继续io.ReadFull(conn, ...)或循环Read()直到返回io.EOF或超时 - 客户端收到 FIN 后,
Read()返回0, nil,此时才应退出读循环并关闭自己 - 若客户端不主动关闭,服务端不能无限等待——需设读超时,例如
conn.SetReadDeadline(time.Now().Add(10 * time.Second)) - 不要在
CloseWrite()后立刻conn.Close(),否则可能触发 RST,影响对端优雅终止
实际中最容易被忽略的点是:心跳和半关闭逻辑混在同一连接上时,SetReadDeadline 的调用时机必须精确到每次 I/O 前,而不是只设一次。一次漏设,整条连接就可能“静默假活”数分钟。











