ReadFrom/WriteTo不能替代半关闭,它们仅被动响应EOF;CloseRead()/CloseWrite()才是Go中触发FIN的正确封装,但需同步管理状态并避免与Close()混用。

ReadFrom/WriteTo 不是半关闭的替代方案
很多人看到 net.Conn 接口有 ReadFrom 和 WriteTo,就以为它们能配合半关闭用——其实不能。ReadFrom 是把远端数据流一次性读完并写入目标 io.Writer,WriteTo 同理;它们内部仍依赖底层 Read/Write,一旦连接进入半关闭(比如对端 shutdown(SHUT_WR)),ReadFrom 会自然返回 io.EOF,但不会触发或控制半关闭行为。它只是“被动响应”,不是“主动建模”。
CloseRead()/CloseWrite() 可用于裸 TCP,但必须配合同步状态管理
net.TCPConn 确实提供了 CloseRead() 和 CloseWrite() 方法,这是 Go 对 shutdown() 的封装,能真正触发 FIN 包发送。但直接调用有风险:
- 调用
CloseWrite()后,后续Write()必然返回write: broken pipe或use of closed network connection - 调用
CloseRead()后,Read()不再阻塞,而是立即返回io.EOF,但已排队未读的数据仍可被读出 - 两个方法都不可逆,且不保证对方立刻感知——对方需主动
Read()才会收到 EOF,否则可能长时间停留在CLOSE_WAIT - 若你在 goroutine 中并发调用
CloseRead()和Write(),必须加锁或用 channel 同步,否则易出现write on closed connection
如何安全实现“读停写不停”的半双工循环
典型场景:服务端发完响应后停止接收新请求,但仍要等客户端确认消息。这时不能只靠 CloseRead(),得结合 context 和显式状态标记:
- 用
ctx, cancel := context.WithCancel(context.Background())控制读循环,收到 EOF 或错误时调cancel() - 读循环退出后,调
conn.CloseRead()(可选,加速对方感知) - 写操作始终检查
ctx.Err() == nil,避免向已取消上下文写入 - 关键点:
CloseRead()后仍可调Write(),但不能再调Read();反之亦然 - 不要在
defer conn.Close()里混用CloseRead()/CloseWrite()——它们和Close()互斥,重复调用会 panic
WebSocket 场景下误用 CloseWrite() 会导致协议损坏
这是最容易踩的坑:gorilla/websocket.Conn 底层虽基于 net.Conn,但它的 WriteMessage() 和 ReadMessage() 是协议层封装。直接对底层 net.Conn 调 CloseWrite():
- 客户端收不到合法的
websocket.CloseMessage帧,只能等待超时后报1006 - 服务端下次调
ReadMessage()会立刻返回read: connection closed,而非预期的关闭码 - 正确做法是先发
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(...)),再等对方回关帧,或设 timeout 后调conn.Close() - 如果真需要“只停读”,唯一可靠方式是用 context 控制读 goroutine,而不是碰连接本身
CLOSE_WAIT 或泄漏 goroutine。


















