bufio.newreader 比 os.file.read 快,因其用用户态缓冲区(默认4096字节)批量读取,大幅减少系统调用次数;适用逐行日志、csv等场景,不适用大文件单次读取。

直接用 os.File.Read 逐行读日志或 CSV,性能差的根因不是 Go 慢,而是每行一次系统调用——10 万次内核切换吃掉 90% CPU 时间;加 bufio 不是“套个壳”,是把零散读请求攒成批量 syscall,实测吞吐可翻 5–10 倍。
bufio.NewReader 为什么比 os.File.Read 快
因为 os.File.Read 每次调用都触发一次系统调用,而 bufio.NewReader 在用户态维护一块缓冲区(默认 bufio.DefaultBufferSize = 4096),一次 read 系统调用读入多字节,后续读操作直接从内存取。它不减少总 I/O 量,只大幅降低 syscall 次数。
- 适用场景:逐行读日志、解析 CSV、HTTP 响应体、TCP 流按分隔符切消息
- 不适用场景:读一个 2MB 的图片文件——此时
io.ReadAll或os.ReadFile更合适,避免多余内存拷贝 - 缓冲区不是越大越好:超过 64KB 后 syscall 优化收益极小,还可能拖慢 GC;SSD 顺序读建议
64 * 1024,网络响应建议4 * 1024–32 * 1024 - 注意:
bufio.Reader的Read方法可能返回io.EOF,但内部缓冲区里还有未消费的数据;用Peek或UnreadRune时要小心边界
bufio.Scanner 报 scanner: token too long 怎么办
这不是文件太大,是单行超长(比如嵌了 base64 或堆栈的错误日志)撑爆了默认 64KB 缓冲区。scanner.Scan() 内部 panic,且无法 recover,服务容易挂;它不报错,只静默返回 false。
- 必须显式扩容:
scanner.Buffer(make([]byte, 64*1024), 1024*1024)(第二参数是最大 token 长度,设为 1MB) - 别设
math.MaxInt32:万一输入缺换行符,会尝试分配数 GB 内存,直接 OOM -
scanner.Text()返回的字符串底层复用缓冲区,下一次Scan()就覆盖;若需长期保存(如存入 map),必须显式拷贝:string(scanner.Bytes())或append([]byte{}, scanner.Bytes()) - 不可控输入(如用户上传文本)别依赖
Scanner;改用bufio.Reader.ReadBytes('\n')自己拼接,自己控制内存和错误处理
bufio.NewWriter 写完文件却为空?检查 Flush
bufio.NewWriter 的缓冲区只在三种情况下真正落盘:缓冲区满、显式调 w.Flush()、或 w.Close()。忘记 Flush 是“写完了但文件空”“程序退出后内容消失”的根源。
- 典型错误:
w := bufio.NewWriter(f); w.WriteString("hello"); // 忘了 flush!—— 这时内容还卡在内存里 - 别只靠
defer w.Close():虽然Close()会尝试Flush(),但若磁盘满导致Flush()失败,Close()也失败,而你没检查返回值就以为成功了 - 更稳妥收尾:
defer func() { if err := w.Flush(); err != nil { /* 处理 err */ } }() - 网络写(如
net.Conn)更要小心:Flush失败意味着底层连接已断,此时再写会 panic,得提前检查err
Reader 和 Scanner 能不能混用同一个 *os.File
不能。混用会导致读位置错乱:一个实例调 Seek 或读了一段,另一个实例的缓冲区还缓着旧数据,下次读先吐缓存,再从新 offset 继续,逻辑完全错位。
- 所有
bufio实例都依赖底层*os.File的 offset;file.Seek()后必须调reader.Reset(file)刷新缓冲区,否则下次读先吐缓存旧数据 - 并发读同一
*os.File时,每个 goroutine 必须用独立的*bufio.Reader实例,否则共享文件 offset + 各自缓冲 = 数据错位 -
bufio.Scanner是带预置分隔逻辑的Reader封装,适合读行、读空格分隔字段;bufio.Reader是裸缓冲,适合精确控制读取边界(比如协议头+变长体) - 混用
Scanner和Reader于同一文件,哪怕只是想“先读几行再交给 Scanner”,也会因缓冲区未同步而丢数据或重复读
最常被忽略的是缓冲区生命周期与底层连接/文件的对齐:关连接早了,Reader 还在缓存里读;Seek 后忘了 Reset,它还在吐上一段缓存;写完不 Flush,你以为完成了,其实全在内存里。这些不是边界情况,是日常踩坑高频点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











