io.Copy 不能直接用于 stdin/stdout 管道,需用 bufio.NewReader 包装并正确处理 Read 返回的 n 和 io.EOF,避免数据丢失和性能抖动。

io.Copy 是流式管道的基石,但不能直接连 stdin/stdout
Go 里用管道(如 cat big.log | ./transform)处理大文件时,很多人直接对 os.Stdin 调 io.Copy 写到 os.Stdout,看似简洁,实则埋雷。问题不在 io.Copy 本身,而在于它默认不校验读取长度、不区分 EOF 前的有效数据,且对管道这种低延迟、非阻塞 I/O 容易误判“读完”。
- 必须用
bufio.NewReader(os.Stdin)包一层,避免系统调用抖动导致小块读取放大开销 - 不要忽略
Read返回的n——_, err := reader.Read(buf)是典型错误,应始终用n, err := reader.Read(buf) - 遇到
err == io.EOF时,只要n > 0就得先处理这批数据,否则末尾几 KB 会丢 - 缓冲区别在循环里反复
make([]byte, 1,复用同一底层数组更稳;<code>bufio.Reader的内部 buffer 才能真正生效
用 io.Pipe 实现阶段解耦时,goroutine 和 close 必须成对
io.Pipe 看似轻量,但它是同步阻塞通道,只适合单生产者+单消费者跨 goroutine 场景。常见错误是把写逻辑写在主 goroutine,导致整个程序卡死。
- 写端必须启动 goroutine:
go func() { defer pw.Close(); process(pw) }() - 写入出错时用
pw.CloseWithError(err),而非pw.Close(),否则 reader 侧无法感知失败 - reader 侧检查
err时,要区分io.EOF(正常结束)、io.ErrClosedPipe(写端 panic 或提前退出)、其它真实错误(如磁盘满) - 别给
io.Pipe加缓冲:io.Pipe本身不支持缓冲区参数,所谓“缓冲”靠的是两端 goroutine 的调度节奏
多阶段转换别硬套 channel 管道,函数组合更轻量
不是所有流式处理都该用 chan []byte 拼 pipeline。比如只是做 strings.TrimSpace → json.Unmarshal → strings.ToUpper,用 channel 反而引入 goroutine 调度、内存分配、关闭管理三重开销。
- 只有满足以下任一条件才值得上 channel:
数据源是 bufio.Scanner、某阶段需并发(如调外部 API)、需要背压(下游慢时上游自动降速)、阶段生命周期不同(如一个长期监听信号) - 扇出(fan-out)时,输入 channel 建议带小缓冲(如
make(chan []byte, 8)),防止单个 worker 卡住拖垮全部 - 扇入(fan-in)时,绝不能让多个 goroutine 都
close(out),要用errgroup.Group或sync.WaitGroup+ 计数器确保只关一次 - 每个
close(out)必须和它对应的for range in在同一个 goroutine 里完成,跨 goroutine 关闭会触发panic: close of closed channel
加密/压缩等 IO 密集阶段,StreamWriter 是比手动分块更稳的选择
对大文件做 AES-CTR 或 gzip 流式处理时,有人倾向手写 for { n, _ := reader.Read(buf); cipher.XORKeyStream(buf[:n], buf[:n]); writer.Write(buf[:n]) },这容易漏掉边界 case,且性能不如标准封装。
立即学习“go语言免费学习笔记(深入)”;
- 优先用
cipher.StreamWriter或gzip.NewWriter这类已封装好的io.Writer实现,它们内部已处理 flush、partial write、error 透传 - 若需控制缓冲大小,用
bufio.NewWriterSize(writer, 64 包一层,而不是改 <code>io.Copy的底层 buffer -
io.Copy默认 32KB 缓冲足够通用场景;只有在高吞吐网络代理或磁盘 IO 明显成为瓶颈时,才考虑io.CopyBuffer自定义缓冲区 - 注意
gzip.NewWriter必须显式调Close()才能写出 footer,否则解压端会报 “unexpected EOF”
io.Copy,而是当管道挂了、上游断连、中间 stage panic、最后一块数据被截半时,整条链路是否还能干净退出、错误是否可追溯、资源是否全释放。这些细节不会出现在示例代码里,但会出现在凌晨三点的线上日志中。


















