第 31 课:缓冲 IO 与 bufio
在上一课中,我们学习了使用 os 包进行基础的文件操作。直接使用 os.File 进行读写虽然直接,但频繁的小数据量读写会引发大量的系统调用,从而降低程序效率。本课将介绍Go标准库中的 bufio 包,它通过引入内存缓冲区,极大地提升了IO操作的性能,是处理文件、网络连接等IO流的必备工具。
1. 学习目标
- 理解缓冲IO(Buffered IO)的原理及其相较于直接IO的优势。
- 掌握使用
bufio.Reader进行高效的数据读取,特别是逐行读取。 - 掌握使用
bufio.Writer进行高效的数据写入,并理解刷新(Flush)的重要性。 - 熟练运用
bufio.Scanner进行灵活、便捷的文本扫描与解析。
2. 核心概念
什么是缓冲IO?
想象一下,你要从仓库(磁盘/网络)搬运很多小包裹(数据)到收件人(你的程序)手中。每次搬一个都亲自去仓库,来回奔波非常耗时(这就是“系统调用”开销)。缓冲IO 就像找了一个中间仓库(内存缓冲区),你先把多个小包裹从大仓库搬到中间仓库(一次性读取大量数据),然后再从中间仓库快速地将包裹逐个交给收件人。反之,写数据时,你先把多个包裹集中放在中间仓库,等仓库满了或你明确说“发货”,再一次性将它们运回大仓库。
bufio 包 正是这个“中间仓库”的管理者。它封装了底层的 io.Reader 和 io.Writer 接口,为它们添加了缓冲功能。
核心结构
bufio.Reader:缓冲读取器。它持有一个内部缓冲区(默认4KB)。当调用Read方法时,它优先从缓冲区读取数据;当缓冲区空了,才会触发一次底层读取操作来填充缓冲区。bufio.Writer:缓冲写入器。它同样持有一个内部缓冲区。当调用Write方法时,数据先写入缓冲区;当缓冲区满了或显式调用Flush()时,才会将缓冲区数据真正写入底层。bufio.Scanner:一个更高级的工具,它构建在bufio.Reader之上,提供了按特定规则(如按行、按单词)“扫描”输入流的能力,非常适合处理文本文件。
3. 代码示例
示例1:使用 bufio.Reader 读取文件
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
// 1. 打开一个文件,获取一个 *os.File,它实现了 io.Reader 接口
file, err := os.Open("example.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 2. 用文件创建一个缓冲读取器
reader := bufio.NewReader(file)
// 3. 使用 ReadString 方法按行读取,直到文件末尾
for {
line, err := reader.ReadString('\n') // 以换行符‘\n’作为分隔符
if err != nil {
if err.Error() == "EOF" {
break // 文件读取完毕
}
log.Fatal(err)
}
fmt.Print(line) // Print 保留了行尾的换行符
}
fmt.Println("\n--- 文件读取完毕 ---")
}
解释:我们创建了一个缓冲读取器 reader。在 for 循环中,ReadString('\n') 会一直读取,直到遇到换行符或文件末尾(EOF)。缓冲机制使得内部可能一次就读取了多行数据到内存,后续的 ReadString 调用直接从内存缓冲区获取数据,速度极快。
示例2:使用 bufio.Scanner 更优雅地读取
Scanner 是处理文本流更推荐的方式。
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
func main() {
// 从一个字符串(实现了 io.Reader 接口)创建 Scanner
input := "Hello, this is line one.\nThis is line two.\nAnd the third line."
scanner := bufio.NewScanner(strings.NewReader(input))
// Scan 方法会按行(默认)迭代,直到没有更多数据
for scanner.Scan() {
line := scanner.Text() // 获取当前扫描到的文本
fmt.Println("扫描到一行:", line)
}
// 检查扫描过程中是否有错误
if err := scanner.Err(); err != nil {
log.Fatal("扫描错误:", err)
}
// 也可以用在文件上
fmt.Println("\n--- 从文件扫描 ---")
file, _ := os.Open("example.txt")
defer file.Close()
fileScanner := bufio.NewScanner(file)
for fileScanner.Scan() {
fmt.Println(fileScanner.Text())
}
}
解释:Scanner 的用法非常简洁。Scan() 按行读取,Text() 返回当前行的内容(不包括换行符)。循环会在输入流结束时自动停止。
示例3:使用 bufio.Writer 高效写入
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
// 创建一个用于写入的文件
outFile, err := os.Create("buffered_output.txt")
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
// 用文件创建一个缓冲写入器
writer := bufio.NewWriter(outFile)
// 向缓冲区写入数据
numLines := 1000
for i := 0; i < numLines; i++ {
// Fprintf 会将数据写入 writer 的缓冲区,而非直接写入文件
_, err := fmt.Fprintf(writer, "这是第 %d 行数据\n", i+1)
if err != nil {
log.Fatal(err)
}
}
// 关键一步:将缓冲区中剩余的数据全部写入底层文件
err = writer.Flush()
if err != nil {
log.Fatal("刷新缓冲区失败:", err)
}
fmt.Printf("成功向文件写入了 %d 行数据。\n", numLines)
}
解释:我们循环调用 Fprintf 将数据写入 writer 的缓冲区。只有最后调用了 Flush(),缓冲区中的数据才会被真正写入 buffered_output.txt 文件。忘记 Flush() 是最常见的错误,会导致数据丢失!
4. 实践练习
练习 1:基础 - 统计文件行数
使用 bufio.Scanner 编写一个函数 countLines(filename string) (int, error),统计指定文件的行数。
预期:对于包含N行文本的文件,函数返回N。
练习 2:进阶 - 处理CSV数据
假设有一个CSV文件 data.csv,内容如下:
Name,Age,City
Alice,30,New York
Bob,25,London
Charlie,35,Paris
使用 bufio.Reader 的 ReadString 或 bufio.Scanner 读取该文件。跳过第一行(标题行),然后打印出所有年龄(第二列)大于28的记录。
预期输出:
Alice,30,New York
Charlie,35,Paris
练习 3:挑战 - 实现简单的 grep 命令
编写一个程序,接受两个命令行参数:模式 和 文件名。程序使用 bufio.Scanner 逐行读取文件,如果某行包含指定的模式(使用 strings.Contains 判断),则将该行打印到标准输出。
运行方式:./mygrep "error" server.log
预期:程序输出 server.log 文件中所有包含 “error” 的行。
5. 常见错误
- 忘记刷新(Flush)Writer:这是使用
bufio.Writer时最致命的错误。数据可能只存在于内存缓冲区中,程序结束后丢失。务必在写入操作完成后、或关闭文件前,调用Flush()。 - 不理解
Scanner的默认分割行为:bufio.NewScanner默认的分割函数是ScanLines(按行分割)。如果你的输入不是以换行符结尾,或者你需要按单词、按字节扫描,需要使用scanner.Split()方法设置自定义的分割函数。 - 缓冲区大小设置不合理:
bufio.NewReaderSize和bufio.NewWriterSize允许你自定义缓冲区大小。对于常规文件,默认的4KB通常足够。处理超大文件或特定格式时,可以适当调整。太小的缓冲区会削弱性能优势,过大的缓冲区则浪费内存。 - 在循环中错误地检查
EOF:直接使用Read或ReadString时,EOF不是一个真正的“错误”,它表示输入结束。在for循环中,应将其作为正常退出条件,而不是使用log.Fatal等致命处理。
6. 小结
- 缓冲IO 通过在内存中设立缓冲区,减少系统调用次数,显著提升IO密集型程序的性能。
bufio.Reader适用于高效读取,特别是ReadString('\n')可方便地实现逐行读取。bufio.Writer用于高效写入,但必须牢记在合适时机调用Flush()以确保数据写入。bufio.Scanner是处理文本流的首选工具,提供了简洁、强大的扫描和解析能力。- 无论是读取还是写入,理解
bufio包底层是建立在io.Reader和io.Writer接口之上的,这使得它可以与任何实现了这些接口的类型(如文件、网络连接、字符串等)无缝协作。
掌握 bufio 包,你就掌握了在Go中进行高效IO操作的一把利器,为后续处理更复杂的网络通信和数据流打下坚实基础。