不是所有文件都应该一次读进内存
入门时我们常用:
data, err := os.ReadFile("access.log")
这很简单,适合小文件。但如果文件有几百 MB,甚至几个 GB,一次读进内存就不合适。日志分析、CSV 处理、文本转换、命令行过滤工具,都更适合边读边处理。Go 标准库的 bufio 包就是为这类场景准备的。
bufio.Scanner 适合逐行扫描,写起来很方便;bufio.Reader 适合更细控制读取;bufio.Writer 可以缓冲写入,减少系统调用。掌握它们后,你就能写出更稳的文本处理工具。
这篇文章用日志统计做例子,讲解 Scanner、Reader 和 Writer 的常见用法。
Scanner 逐行读取
假设日志每行一个请求:
GET / 200
GET /api/users 200
POST /api/login 401
逐行读取:
func countLines(path string) (int, error) {
file, err := os.Open(path)
if err != nil {
return 0, fmt.Errorf("open file: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
count := 0
for scanner.Scan() {
count++
}
if err := scanner.Err(); err != nil {
return 0, fmt.Errorf("scan file: %w", err)
}
return count, nil
}
scanner.Scan() 每次读取一行,scanner.Text() 可以拿到当前行。循环结束后必须检查 scanner.Err(),否则读取中途出错会被忽略。
统计状态码:
func countStatus(path string) (map[string]int, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
counts := make(map[string]int)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 3 {
continue
}
status := fields[2]
counts[status]++
}
if err := scanner.Err(); err != nil {
return nil, err
}
return counts, nil
}
这段代码不会把整个日志读进内存,只保存状态码计数。
Scanner 默认有单行长度限制
Scanner 默认 token 最大长度有限,处理特别长的一行时可能报错。可以调大缓冲:
scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
这表示初始缓冲 64 KB,最大 1 MB。如果你要处理可能超长的行,比如压缩后的 JSON 单行日志,要注意这个限制。
如果每行可能非常大,bufio.Reader 有时更合适。
Reader 提供更灵活读取
使用 Reader.ReadString('\n'):
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
if line != "" {
fmt.Print(line)
}
break
}
return err
}
fmt.Print(line)
}
ReadString 会读到分隔符为止。如果最后一行没有换行符,遇到 io.EOF 时仍可能返回一段数据,所以要处理 line != ""。
Reader 比 Scanner 稍啰嗦,但控制力更强。简单逐行统计优先用 Scanner,需要处理超长行或自定义读取策略时,再考虑 Reader。
Writer 缓冲输出
大量写入时,可以用 bufio.Writer:
func writeReport(path string, counts map[string]int) error {
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("create report: %w", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
defer writer.Flush()
for status, count := range counts {
if _, err := fmt.Fprintf(writer, "%s,%d\n", status, count); err != nil {
return fmt.Errorf("write report: %w", err)
}
}
return nil
}
Flush 很重要。缓冲写入的数据不一定立刻落到文件,忘记 Flush 可能导致文件内容不完整。为了处理 Flush 错误,更严谨的写法是:
if err := writer.Flush(); err != nil {
return fmt.Errorf("flush report: %w", err)
}
如果使用 defer writer.Flush(),错误不容易处理。命令行小工具可以接受,关键数据写入最好显式检查。
小结
bufio 是 Go 文本处理的基础工具。Scanner 适合逐行读取,代码简洁;Reader 适合更灵活的读取控制;Writer 适合缓冲输出,但要记得 Flush。处理大文件时,边读边算比一次读入内存更可靠。
入门阶段可以记住一个判断:小文件、配置文件用 os.ReadFile 很方便;日志、导出文件、长文本和流式输入,用 bufio.Scanner 或 Reader 更稳。工具选对了,程序在数据变大时才不会突然撑爆内存。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。