
当使用 io.copy 在网络连接与标准流之间复制数据时,若远程 socket 关闭,io.copy 会返回 io.eof 错误;通过检查该错误即可及时感知断连并优雅退出 goroutine。
当使用 io.copy 在网络连接与标准流之间复制数据时,若远程 socket 关闭,io.copy 会返回 io.eof 错误;通过检查该错误即可及时感知断连并优雅退出 goroutine。
在 Go 中,io.Copy(dst, src) 是阻塞式操作,它会持续从 src 读取数据并写入 dst,直到 src.Read 返回 (0, io.EOF)(表示读取结束)或发生其他错误。*关键点在于:当底层 TCP 连接被对端关闭(如 FIN 包到达)或异常中断(如 RST、网络闪断)时,conn.Read 会立即返回 (0, io.EOF) 或具体网络错误(如 `net.OpError),进而使io.Copy` 返回该错误**——而非静默挂起。
因此,原始代码中直接调用 io.Copy(...) 而不检查返回值,会导致 goroutine 无法感知断连,只能依赖 select{} 空阻塞,既浪费资源又丧失控制力。
✅ 正确做法是显式检查 io.Copy 的返回值:
go func() {
defer conn.Close()
defer stdin.Close()
_, err := io.Copy(stdin, conn)
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
log.Println("Remote socket closed gracefully")
} else {
log.Printf("Copy stdin→conn failed: %v", err)
}
}
}()
go func() {
defer conn.Close()
defer stdout.Close()
defer stderr.Close()
// 注意:io.Copy(conn, stdout) 和 io.Copy(conn, stderr) 是顺序执行,
// 若 stdout 先 EOF,stderr 不会自动开始复制(除非手动处理多路复用)
// 实际中建议用 io.MultiWriter 或 goroutine 分别处理,此处为简化逻辑
_, err1 := io.Copy(conn, stdout)
_, err2 := io.Copy(conn, stderr)
if err1 != nil && !errors.Is(err1, io.EOF) {
log.Printf("Copy stdout→conn failed: %v", err1)
}
if err2 != nil && !errors.Is(err2, io.EOF) {
log.Printf("Copy stderr→conn failed: %v", err2)
}
}()
⚠️ 注意事项:
-
io.EOF表示对端正常关闭连接(如调用CloseWrite()或进程退出),属于预期终止信号; - 其他错误(如
read: connection reset by peer、use of closed network connection)则代表异常断连,需按故障处理; -
conn.Close()是幂等的,重复调用无副作用,但应在所有io.Copy退出后统一关闭,避免竞态; - 若需同时监听多个流(如 stdout/stderr)并实时转发,应使用
io.MultiWriter(conn)+ 单个io.Copy,或为每个流启独立 goroutine 并通过sync.WaitGroup协同退出; - 不要依赖
select{}空循环维持程序运行——改用sync.WaitGroup等待所有复制 goroutine 结束更健壮。
总结:io.Copy 的返回值是连接状态的权威信源。始终检查其 error,区分 io.EOF 与其他错误,并据此触发清理与退出逻辑,才能实现可靠、可观察的网络流代理。










