Skip to content
Go
bufio — 缓冲 I/O

bufio — 缓冲 I/O

bufioio.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 之前会用完数据。用 ReadStringReadBytes


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"

自定义分割函数的执行逻辑

  1. Scanner 读取数据到内部缓冲区。
  2. Scanner 调用你的分割函数,传入缓冲区的未处理部分和 atEOF 标志。
  3. 你的函数找到分隔符 → 返回 token + advance > 0。
  4. 你的函数没找到分隔符 + atEOF == false → 返回 (0, nil, nil) 让 Scanner 继续读取。
  5. 你的函数没找到分隔符 + 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