Go I/O性能瓶颈主因是小块频繁调用和内存乱分配;应使用bufio缓存、sync.Pool复用缓冲区、流式分块读写、合理控制并发度并预分配空间。
Go 程序的 I/O 性能瓶颈,八成出在“小块频繁调用”和“内存乱分配”上——不是硬盘或网卡慢,而是你每写一行日志就 Write 一次,每次读一个包就 Read 一次,还顺手 new 了十次 []byte。优化不是换库,是让每次系统调用干更多活、让每次内存分配更可控。
bufio 包装文件和网络连接,别裸调 os.File.Read
裸调 os.File.Read 或 conn.Read 每次都触发 syscall,开销远高于内存拷贝。而 bufio.Reader 和 bufio.Writer 在用户态缓存数据,把 N 次小读写聚合成 1–2 次大 I/O。
bufio.NewReaderSize(f, 64*1024) 显式设为 64KB 更稳bufio.Scanner 适合按行处理(如解析日志),但注意它内部会自动扩容,若行长不可控,改用 reader.ReadString('\n') + strings.T
rimSpace 更可控writer.Flush(),否则数据可能滞留在缓冲区不落盘;HTTP 响应体等流式写入场景,可配合 http.ResponseWriter 的 Flusher 接口做实时推送new bufio.Reader:复用一个实例,或从 sync.Pool 获取os.ReadFile,分块读 + io.CopyBuffer 更安全os.ReadFile 简洁,但会把整个文件加载进内存——1GB 文件直接 OOM。真实场景该流式处理,边读边转、边读边传。
os.Open 打开文件,再套 bufio.NewReader 或直接用 io.CopyBuffer(dst, src, make([]byte, 64*1024)),显式复用缓冲区避免反复分配io.Copy 内部已优化块大小,但若目标支持 WriteAt(如本地磁盘),可结合 file.Seek + 分片并发读,注意 SSD 上并发 8–16 路较优,HDD 则 2–4 路更稳output.Truncate(size),减少文件系统元数据更新和碎片os.O_APPEND 标志,保证原子性,避免多个 goroutine 写同一文件时覆盖sync.Pool 复用缓冲区比 make([]byte, n) 省 30%+ GC 压力HTTP 服务每秒处理上千请求,每个请求都 make([]byte, 4096),GC 会频繁扫描这些短期对象。用 sync.Pool 缓存常见尺寸的切片,效果立竿见影。
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 0, 32*1024) }}
buf := bufPool.Get().([]byte); buf = buf[:0]; ...; bufPool.Put(buf)
buf = buf[:0]),避免跨请求泄露数据[]byte 或 bytes.Buffer
开 100 个 goroutine 同时读文件,磁盘寻道和内核锁反而让吞吐暴跌。I/O 并发的关键不是数量,是“错开等待”,并避开硬件瓶颈。
iostat -x 1 看 %util 是否持续 >90%sem := make(chan struct{}, 8),每个 goroutine 先 sem 再操作,完成后
os.File 内部有 syscall.Seek 隔离),但写必须串行:要么加 sync.Mutex,要么用单个 writer goroutine 消费 channel 输入errgroup.Group + ctx.WithTimeout 统一控制生命周期和错误传播最常被忽略的点:缓冲区大小不是越大越好,64KB 是多数场景的甜点,再大容易浪费内存且不提升吞吐;还有就是 Flush() 容易忘,一忘就丢数据——尤其在程序异常退出时,记得用 defer writer.Flush() 或在关键路径显式调用。