bufio — 缓冲 I/O
bufio 在 io.Reader / io.Writer 之上添加缓冲层,减少系统调用次数。对于频繁的小读写,使用 bufio 可以显著提升性能。
1. bufio.Reader
1.1 🔬 为什么需要缓冲?
// 没有缓冲:每次 Read 可能产生一次系统调用
for {
n, err := file.Read(buf) // ← 每次都是 syscall
}
// 有缓冲:Reader 预读一大块到内存,后续 Read 从内存取
reader := bufio.NewReader(file) // 默认 4KB 缓冲区
for {
n, err := reader.Read(buf) // ← 只有缓冲区空了才 syscall
}1.2 常用方法
reader := bufio.NewReader(file)
// 或自定义缓冲区大小
reader := bufio.NewReaderSize(file, 64*1024) // 64KB
// 读取到分隔符(包含分隔符)
line, err := reader.ReadString('\n') // 返回 string
line, err := reader.ReadBytes('\n') // 返回 []byte
// 预览但不读取
peeked, err := reader.Peek(4) // 预览前 4 字节
// 丢弃 N 字节
reader.Discard(1024)
// 获取内部缓冲区大小
fmt.Println(reader.Size()) // 4096
fmt.Println(reader.Buffered()) // 缓冲区中未读的字节数1.3 ReadSlice vs ReadBytes vs ReadString
| 方法 | 返回值 | 注意事项 |
|---|---|---|
ReadSlice(delim) |
[]byte 指向内部 buffer |
⚠️ 最危险:返回的切片可能在下一次 Read 时失效 |
ReadBytes(delim) |
[]byte 独立拷贝 |
安全但可能分配 |
ReadString(delim) |
string 独立拷贝 |
等价于 string(ReadBytes(delim)) |
💡 最佳实践:永远不要用
ReadSlice,除非你确定在下次 Read 之前会用完数据。用ReadString或ReadBytes。
2. bufio.Writer
2.1 基础用法
// 创建
writer := bufio.NewWriter(file)
// 或自定义大小
writer := bufio.NewWriterSize(file, 64*1024)
// 写入(写入缓冲区,不一定立即落盘)
writer.WriteString("hello\n")
writer.Write([]byte("world\n"))
writer.WriteByte('!')
// 关键:Flush 将缓冲数据真正写入底层 Writer
writer.Flush() // ← 必须调用!🚨 顶级陷阱:忘记
Flush()是bufio.Writer最常见的错误。数据留在缓冲区中永远不会被写入。所有写入结束后必须 Flush。
2.2 查看缓冲区状态
fmt.Println(writer.Available()) // 剩余可用缓冲区大小
fmt.Println(writer.Buffered()) // 缓冲区中已缓存的字节数3. bufio.Scanner — 推荐的按行读取方式
3.1 基础用法
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text() // 获取当前行(不含换行符)
fmt.Println(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}3.2 自定义分割函数
// 按单词分割
scanner.Split(bufio.ScanWords)
// 按字节分割
scanner.Split(bufio.ScanBytes)3.3 🔬 自定义分割函数详解
分割函数签名:func(data []byte, atEOF bool) (advance int, token []byte, err error)
| 参数/返回值 | 说明 |
|---|---|
data |
缓冲区中未处理的数据(不是整个输入!) |
atEOF |
是否已到达输入末尾(true 表示没有更多数据了) |
advance |
前进的字节数:告诉 Scanner 已经消耗了多少数据(返回 0 表示需要更多数据) |
token |
本次输出的 token:返回给 scanner.Bytes() 或 scanner.Text() 的数据 |
err |
非 nil 时 Scan 停止,通常会返回 bufio.ErrFinalToken 等 |
// 自定义分割函数:按两个连续的换行符分割(空行分隔段落)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
// 1. 查找分隔符 "\n\n" 的位置
if i := bytes.Index(data, []byte("\n\n")); i >= 0 {
// 找到了:i 是第一个 \n 的位置
// advance = i + 2(跳过两个 \n),token 是分隔符之前的数据
return i + 2, data[:i], nil
}
// 2. 如果已到 EOF 且还有剩余数据,返回剩余全部
if atEOF && len(data) > 0 {
return len(data), data, nil
}
// 3. 还没找到分隔符且没到 EOF:返回 0 告诉 Scanner 需要更多数据
// Scanner 会继续读取并再次调用此函数
return 0, nil, nil
})
// 示例:解析 "a\n\nb\n\nc" → 3 个 token: "a", "b", "c"自定义分割函数的执行逻辑:
- Scanner 读取数据到内部缓冲区。
- Scanner 调用你的分割函数,传入缓冲区的未处理部分和 atEOF 标志。
- 你的函数找到分隔符 → 返回 token + advance > 0。
- 你的函数没找到分隔符 +
atEOF == false→ 返回(0, nil, nil)让 Scanner 继续读取。 - 你的函数没找到分隔符 +
atEOF == true→ 返回剩余数据(或返回 error 中止)。
// 更复杂的例子:按自定义开始/结束标记分割
// 例如提取 XML 中 <tag>...</tag> 的内容
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
// 查找开始标记 <tag> 和结束标记 </tag>
startTag := []byte("<tag>")
endTag := []byte("</tag>")
startIdx := bytes.Index(data, startTag)
if startIdx == -1 {
// 没找到开始标记:跳过当前所有已检查的数据,等更多数据
if atEOF {
return len(data), nil, nil // 放弃剩余数据
}
// 保留最后 len(startTag)-1 个字节,防止开始标记跨缓冲区边界
if len(data) > len(startTag) {
return len(data) - len(startTag) + 1, nil, nil
}
return 0, nil, nil // 数据还不够
}
// 从开始标记之后查找结束标记
endIdx := bytes.Index(data[startIdx+len(startTag):], endTag)
if endIdx == -1 {
if atEOF {
return len(data), nil, nil // 放弃
}
return 0, nil, nil // 等待更多数据
}
// 计算 token 位置和 advance
contentStart := startIdx + len(startTag)
contentEnd := startIdx + len(startTag) + endIdx
totalAdvance := contentEnd + len(endTag) // 跳过整个 <tag>...</tag>
return totalAdvance, data[contentStart:contentEnd], nil
})3.4 处理超长行
// 默认 token 最大 64KB
// 超长行会返回 bufio.ErrTooLong
// 解决方案:
scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 1024*1024) // 1MB
scanner.Buffer(buf, 10*1024*1024) // 最大 10MB
for scanner.Scan() {
// 处理行
}4. Buffered I/O 性能对比
一个直接的对比:向文件写入 100,000 行。
// 不带缓冲直接写 ~10ms
for i := 0; i < 100000; i++ {
file.WriteString("line\n")
}
// 带 bufio.Writer ~2ms(5x 提升)
writer := bufio.NewWriter(file)
for i := 0; i < 100000; i++ {
writer.WriteString("line\n")
}
writer.Flush()5. 🚨 常见陷阱
| 陷阱 | 说明 |
|---|---|
Writer 不 Flush |
数据丢失 |
Scanner 不检查 Err() |
默默截断数据 |
ReadSlice 长期持有返回值 |
数据被覆盖 |
| Scanner 与 Reader 混用 | Scanner 内部也使用了缓冲 |
| 默认 64KB 行限制 | 数据中有长行时报 ErrTooLong |