使用 bufio.Writer 是最直接、最稳妥的方案,它能将成百上千次小写入合并为几次系统调用,性能提升通常在 5–50 倍之间;其通过内存缓冲区暂存数据,仅在缓冲满、显式 Flush() 或关闭时触发系统调用,避免 os.File.Write 每次写入都引发用户态/内核态切换开销。

用 bufio.Writer 是最直接、最稳妥的方案,它能将成百上千次小写入合并为几次系统调用,性能提升通常在 5–50 倍之间,具体取决于数据块大小和频率。
为什么 os.File.Write 会触发太多系统调用
每次调用 os.File.Write(或 file.Write)时,Go 会把传入的 []byte 直接交给内核的 write() 系统调用。哪怕只写 12 个字节,也得切换一次用户态/内核态——这个开销在高并发或高频写入(如日志打点、指标上报)下会被急剧放大。
常见错误现象:
- 写入 10 万行日志耗时 >2 秒,
strace -e write显示触发了近 10 万次write() - CPU 花费大量时间在
sys_enter_write和上下文切换上,而非业务逻辑 - 磁盘 I/O 队列堆积(
iostat -x 1中 %util 接近 100%,但吞吐却很低)
bufio.Writer 的缓冲行为与关键参数
bufio.Writer 在内存中维护一块缓冲区,写操作先填这里;只有缓冲区满、显式调用 Flush()、或关闭文件时,才真正发起系统调用。
立即学习“go语言免费学习笔记(深入)”;
必须注意的几点:
-
bufio.NewWriter(f)使用默认 4096 字节缓冲,对平均行长 bufio.NewWriterSize(f, 64*1024) - 缓冲区大小不是越大越好:64KB 是多数场景的甜点值;超 1MB 容易导致延迟升高,且无实际收益
-
Flush()必须手动调用,否则程序退出前没落盘,数据就丢了(尤其在 panic 或 os.Exit 场景下) - 如果写入后需立即被其他进程读到(如 watchdog 检查),仅
Flush()不够,还得f.Sync()
并发写入同一文件时的典型陷阱
多个 goroutine 共享一个 *bufio.Writer 会引发竞态:内部缓冲区、偏移量、错误状态都不是并发安全的。即使加 sync.Mutex,锁粒度太粗也会成为瓶颈。
更合理的做法是:
- 用 channel + 单写协程模型:所有生产者发数据到
chan []byte,唯一消费者用bufio.Writer批量写入 - channel 缓冲大小设为 1000–10000,避免内存暴涨或阻塞过久
- 写协程退出前,务必
close(ch)并在循环后调用wr.Flush(),否则最后一段内容可能滞留 - 不要用
log.SetOutput直接设为bufio.Writer——log包内部不保证调用Flush()
什么时候不该用 bufio.Writer
不是所有场景都适合缓冲。以下情况应绕过或慎用:
- 写入关键事务日志(如数据库 WAL),要求每条记录严格落盘:此时应禁用缓冲,直接
file.Write+f.Sync() - 单次写入数据已超 1MB:缓冲反而多一次内存拷贝,直接
file.Write(buf)更高效 - 跨平台分发的 CLI 工具:避免用
syscall.Write,它在 Windows 不可用 - 写入管道(
os.Pipe)或网络连接(net.Conn):bufio.Writer依然有效,但要注意对方接收能力,避免死锁
缓冲区不是银弹——它把「何时写」的控制权从内核交还给了你。忘了 Flush(),数据就卡在内存里;设得太小,等于白搭;并发乱用,轻则丢数据,重则 panic。真正的优化点,往往藏在那行容易被删掉的 wr.Flush() 后面。



















