bufio能提升io性能是因为其用默认4kb缓冲区合并多次小读写,大幅减少系统调用;原生os.file.read等每次调用均触发syscall,而bufio仅在缓冲空或满时才与内核交互,实测性能可差40倍以上。

直接说结论:用 bufio.Reader 和 bufio.Writer 包装底层 io.Reader/io.Writer,绝大多数场景默认 4KB 缓冲就足够;但必须避免混用 bufio.Scanner、bufio.Reader 和 fmt.Scan,否则会丢数据或卡住。
为什么 bufio 能提升 IO 性能
原生 os.File.Read() 或 net.Conn.Read() 每次调用都触发一次系统调用(syscall),小数据频繁读写时开销极大。而 bufio.Reader 在内存里维护一块缓冲区(默认 4096 字节),只有缓冲区空了才从底层读一批数据进来;bufio.Writer 同理,只在缓冲区满、显式 Flush() 或对象被关闭时才真正写入底层。
实测对比:逐字节读取 1MB 文件,原生 Read() 耗时约 80ms,bufio.Reader.Read() 仅约 2ms——性能差 40 倍以上。
- 缓冲区不是越大越好:超过 64KB 后收益明显下降,还浪费内存
- 小文件(io.Copy())几乎无收益,甚至略慢(多一层函数调用)
- 网络连接中,
bufio对吞吐影响远小于对延迟的影响——它主要减少 syscall 次数,不改变 TCP 包大小
bufio.Reader 读取时的常见陷阱
bufio.Reader.ReadString('\n') 看似简单,但实际行为和直觉有偏差:
- 返回的字符串包含结尾的
\n(或\r\n),不是自动 trim 的;要用strings.TrimSuffix(line, "\n")或切片line[:len(line)-1] -
ReadLine()返回[]byte且不保证含换行符;当行超长被截断时,isPrefix == true,需循环拼接 - 用
ReadByte()读 ASCII 字符没问题,但遇到中文等 Unicode 字符会错乱;应改用ReadRune() - 调用
UnreadByte()只能回退一个字节,且不能跨缓冲区边界;多次UnreadByte()后再ReadString()可能 panic
bufio.Writer 必须手动 Flush 的场景
bufio.Writer 不会在写完就立刻落盘或发包,所有内容先攒在缓冲区里。这意味着:
- 程序退出前没
Flush(),最后一段数据就丢了(尤其写文件时) - 网络服务中,
WriteString("OK")后不Flush(),客户端可能永远收不到响应 -
Write()返回成功 ≠ 数据已写出;要确认落盘/发包,必须检查Flush()的 error - 可以复用
Writer:用wr.Reset(newWriter)替换底层io.Writer,避免反复 alloc 缓冲区
Scanner 和 Reader 到底该选谁
别凭感觉,看需求:
- 按行处理日志、用户输入、配置文件?用
bufio.Scanner——它自动去换行符、内置行长度限制、API 更干净 - 需要读单个字符、混合解析(比如先读 4 字节长度头,再读指定字节数 payload)、保留原始换行符、或处理 \r 结尾的旧数据?必须用
bufio.Reader - 绝对不要在一个
os.Stdin上先后创建Scanner和Reader;哪怕只声明了一个Scanner,后续Reader也会读到空——因为Scanner已经把缓冲区预读走了 -
Scanner.Err()必须检查:EOF 是合法终止,但磁盘满、连接断等错误会被静默忽略
最易被忽略的一点:缓冲区大小不是“越大越稳”,而是“刚好够用”。4KB 覆盖 95% 场景;真遇到超长行或批量写入,优先考虑业务逻辑拆分,而不是盲目调大缓冲区——那只是把问题从 syscall 层转移到内存层。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











