Go bufio 包完全实战指南:逐行扫描、缓冲读写与流式处理优化

深入讲解 Go bufio 包的 Reader、Writer 缓冲原理,Scanner 分词机制与自定义 SplitFunc,大文件逐行读取优化策略,HTTP 响应体读取优化,CSV 和 JSON 流式解析中的 bufio 应用,Flush 策略与内存分配优化。

不是所有文件都应该一次读进内存

入门阶段我们常用 os.ReadFile 一次性读取文件内容,这种方式对配置文件和小数据文件非常方便。但当文件增长到几百 MB 甚至几个 GB 时,一次读进内存就不再合适。日志分析、CSV 处理、文本转换、命令行过滤工具,都更适合边读边处理的方式。

Go 标准库的 bufio 包就是为这类场景设计的。它基于 io.Readerio.Writer 接口提供了缓冲层,大幅减少系统调用次数,提升 IO 效率。bufio.Scanner 适合逐行扫描,代码简洁直观;bufio.Reader 提供更精细的读取控制;bufio.Writer 实现缓冲写入,减少写入时的系统调用开销。掌握这些工具后,你就能写出在处理大规模数据时依然稳定高效的程序。

本文从 Scanner 逐行读取开始,深入 Reader 和 Writer 的缓冲原理,展示大文件处理、HTTP 流读取、CSV 和 JSON 流式解析中的实战应用,帮助你建立完整的 bufio 知识体系。

Scanner 逐行读取机制

基本逐行扫描

假设日志文件每行代表一个请求记录:

package main

import (
	"bufio"
	"fmt"
	"os"
)

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() 每次读取一个 token,默认以换行符分割。scanner.Text() 返回当前 token 的字符串形式,scanner.Bytes() 返回字节切片形式。循环结束后必须检查 scanner.Err(),否则读取中途出错会被静默忽略。

统计状态码实战

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func countStatusCodes(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
}

这段代码不会把整个日志读进内存,只保存状态码计数。对于 GB 级别的日志文件,内存占用始终保持在 O(1) 级别。

Scanner 的缓冲限制与调整

Scanner 默认的最大 token 长度是 64KB,处理特别长的一行时会报错:bufio.Scanner: token too long。这在处理 JSON 日志行或压缩数据时很常见。

调大缓冲区

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024) // 最大 1MB

这表示初始缓冲区 64KB,最大可增长到 1MB。如果每行可能非常大,考虑使用 bufio.Reader 来代替 Scanner。

自定义 SplitFunc

Scanner 的强大之处在于可以通过自定义 SplitFunc 实现任意分词逻辑:

// 按段落分割(空行分隔)
func ScanParagraphs(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}

	// 查找两个连续换行
	for i := 0; i < len(data)-1; i++ {
		if data[i] == '\n' && data[i+1] == '\n' {
			return i + 2, data[:i+1], nil
		}
	}

	if atEOF {
		return len(data), data, nil
	}
	return 0, nil, nil // 请求更多数据
}

func main() {
	scanner := bufio.NewScanner(strings.NewReader(text))
	scanner.Split(ScanParagraphs)

	for scanner.Scan() {
		fmt.Println("=== Paragraph ===")
		fmt.Println(scanner.Text())
	}
}

Go 标准库内置了多种 SplitFunc:

函数分割规则
ScanBytes逐字节
ScanRunes逐 Unicode 码点
ScanLines逐行(默认)
ScanWords逐词

bufio.Reader:精细读取控制

当 Scanner 的默认行为不满足需求时,bufio.Reader 提供更灵活的读取能力。

ReadString 逐行读取

reader := bufio.NewReader(file)
for {
	line, err := reader.ReadString('\n')
	if err != nil {
		if err == io.EOF {
			if len(line) > 0 {
				processLine(line)
			}
			break
		}
		return err
	}
	processLine(line)
}

ReadString 读到分隔符为止。如果最后一行没有换行符,遇到 io.EOF 时仍可能返回有效数据,必须处理 line != "" 的情况。

ReadLine:更底层的行读取

reader := bufio.NewReader(file)
for {
	line, isPrefix, err := reader.ReadLine()
	if err != nil {
		if err == io.EOF {
			break
		}
		return err
	}

	if isPrefix {
		// 行太长,需要拼接
		var fullLine []byte
		fullLine = append(fullLine, line...)
		for isPrefix {
			line, isPrefix, err = reader.ReadLine()
			if err != nil {
				return err
			}
			fullLine = append(fullLine, line...)
		}
		processLine(string(fullLine))
	} else {
		processLine(string(line))
	}
}

ReadLine 不返回换行符,且当行超过缓冲区时返回 isPrefix = true。它的开销比 ReadString 稍低,但使用更繁琐。

Peek:预读不消费

reader := bufio.NewReader(file)
peek, err := reader.Peek(10)
if err != nil {
	return err
}

if bytes.HasPrefix(peek, []byte("HEADER")) {
	// 处理头格式
}

Peek(n) 返回接下来的 n 个字节但不移动读取位置。这在协议识别和格式探测中很有用。

ReadBytes 与 ReadSlice

// ReadBytes 读到分隔符,返回切片的副本
line, err := reader.ReadBytes('\n')

// ReadSlice 读到分隔符,返回底层缓冲区的引用(更快但不安全持久化)
line, err := reader.ReadSlice('\n')

ReadSlice 返回的数据可能被后续读取覆盖,如果要长期保存,需要使用 ReadBytes 或自行拷贝。

bufio.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 writer.Flush()
}

Flush 至关重要。缓冲写入的数据不一定立刻落到文件,忘记 Flush 可能导致内容不完整。为了处理 Flush 错误,关键数据写入应显式检查。

批量写入的 Flush 策略

func processLargeOutput(input io.Reader, output io.Writer) error {
	reader := bufio.NewReader(input)
	writer := bufio.NewWriter(output)
	defer writer.Flush()

	linesProcessed := 0
	for {
		line, err := reader.ReadString('\n')
		if err != nil && err != io.EOF {
			return err
		}

		processed := transformLine(line)
		if _, err := writer.WriteString(processed); err != nil {
			return err
		}

		linesProcessed++
		// 每 1000 行主动 Flush,避免数据丢失风险
		if linesProcessed%1000 == 0 {
			if err := writer.Flush(); err != nil {
				return fmt.Errorf("flush failed: %w", err)
			}
		}

		if err == io.EOF {
			break
		}
	}

	return writer.Flush()
}

io.Copy 与 bufio 的协作

func copyFileWithBuffer(src, dst string) (int64, error) {
	in, err := os.Open(src)
	if err != nil {
		return 0, err
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return 0, err
	}
	defer out.Close()

	// io.Copy 内部会根据源和目标的类型选择最优策略
	return io.Copy(out, in)
}

io.Copy 在内部会使用 bufio 的缓冲策略(如果源和目标没有实现 io.WriterTo/io.ReaderFrom)。但你也可以显式包装:

reader := bufio.NewReader(in)
writer := bufio.NewWriter(out)
written, err := io.Copy(writer, reader)
if err != nil {
	return 0, err
}
if err := writer.Flush(); err != nil {
	return 0, err
}

HTTP 响应体读取优化

处理 HTTP 响应时,bufio 可以帮助优化读取性能:

func fetchAndProcess(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// resp.Body 已经是 bufio.Reader 包装过的
	// 对于大多数场景直接使用即可
	scanner := bufio.NewScanner(resp.Body)
	scanner.Buffer(make([]byte, 1024), 1024*1024)

	for scanner.Scan() {
		line := scanner.Text()
		processLine(line)
	}

	return scanner.Err()
}

对于大文件下载,流式处理避免内存堆积:

func downloadToFile(url, path string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	out, err := os.Create(path)
	if err != nil {
		return err
	}
	defer out.Close()

	// io.Copy 会自动处理缓冲
	_, err = io.Copy(out, resp.Body)
	return err
}

CSV 流式解析中的 bufio 应用

对于超大 CSV 文件,流式解析是必须的:

func processLargeCSV(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := csv.NewReader(bufio.NewReader(file))
	reader.ReuseRecord = true // 重用切片减少分配

	// 跳过表头
	if _, err := reader.Read(); err != nil {
		return err
	}

	recordsProcessed := 0
	for {
		record, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("row %d: %w", recordsProcessed, err)
		}

		if err := processRecord(record); err != nil {
			return fmt.Errorf("process row %d: %w", recordsProcessed, err)
		}
		recordsProcessed++

		// 批量提交或大文件时的进度报告
		if recordsProcessed%10000 == 0 {
			fmt.Printf("Processed %d records\n", recordsProcessed)
		}
	}

	return nil
}

csv.Reader 内置了 bufio.Reader 的缓冲机制,配合 ReuseRecord = true 可以大幅减少内存分配。

JSON 流式解析中的 bufio 应用

对于 JSON Lines 格式(每行一个 JSON 对象),使用 json.Decoder 配合 bufio.Scanner

func processJSONLines(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)

	lineNum := 0
	for scanner.Scan() {
		lineNum++
		var event Event
		if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
			return fmt.Errorf("line %d: %w", lineNum, err)
		}

		if err := handleEvent(event); err != nil {
			return fmt.Errorf("line %d: %w", lineNum, err)
		}
	}

	return scanner.Err()
}

对于 JSON 数组的流式解析,使用 json.Decoder

func processJSONArray(r io.Reader) error {
	decoder := json.NewDecoder(bufio.NewReader(r))

	// 读取开始数组标记
	token, err := decoder.Token()
	if err != nil {
		return err
	}
	if token != json.Delim('[') {
		return fmt.Errorf("expected array start")
	}

	count := 0
	for decoder.More() {
		var item Item
		if err := decoder.Decode(&item); err != nil {
			return err
		}
		if err := processItem(item); err != nil {
			return err
		}
		count++
	}

	// 读取结束数组标记
	token, err = decoder.Token()
	if err != nil {
		return err
	}
	if token != json.Delim(']') {
		return fmt.Errorf("expected array end")
	}

	fmt.Printf("Processed %d items\n", count)
	return nil
}

大文件逐行读取的性能优化

处理 GB 级别文件时,以下优化策略非常关键:

1. 合理设置缓冲区大小

// 64KB 缓冲区适合大多数场景
reader := bufio.NewReaderSize(file, 64*1024)

// 对于非常规则的小行(如固定长度记录),可以更大
reader := bufio.NewReaderSize(file, 256*1024)

2. 避免字符串转换(如果可能)

// 如果后续处理只需要字节,避免 string 转换
for scanner.Scan() {
	line := scanner.Bytes() // 字节切片,不重新分配
	processBytes(line)
}

3. 减少内存分配

type LineProcessor struct {
	buffer []byte // 复用缓冲区
}

func (p *LineProcessor) Process(r io.Reader) error {
	reader := bufio.NewReader(r)
	for {
		p.buffer = p.buffer[:0] // 重置但不释放
		line, err := reader.ReadSlice('\n')
		p.buffer = append(p.buffer, line...)

		if err != nil && err != bufio.ErrBufferFull {
			if err == io.EOF {
				if len(p.buffer) > 0 {
					processLine(p.buffer)
				}
				break
			}
			return err
		}
		processLine(p.buffer)
	}
	return nil
}

常见问题与最佳实践

问题 1:忘记检查 scanner.Err()

// 错误:扫描错误被忽略
for scanner.Scan() {
	process(scanner.Text())
}
// scanner.Err() 未被检查!

// 正确
for scanner.Scan() {
	process(scanner.Text())
}
if err := scanner.Err(); err != nil {
	return fmt.Errorf("scan failed: %w", err)
}

问题 2:Flush 处理不当

// 危险:defer 中的 Flush 错误被忽略
writer := bufio.NewWriter(file)
defer writer.Flush() // Flush 的错误被丢弃

// 更好:关键数据显式 Flush
if err := writer.Flush(); err != nil {
	return fmt.Errorf("flush failed: %w", err)
}

问题 3:Reader 和 Scanner 混用

// 错误:同一个底层 reader 被两种包装器读取
reader := bufio.NewReader(file)
scanner := bufio.NewScanner(file)

// 正确:基于同一个底层 reader,或者只用其中一种
scanner := bufio.NewScanner(file)
// 或
reader := bufio.NewReader(file)

最佳实践总结

  1. 小文件用 os.ReadFile,大文件用 bufio.Scanner
  2. Scanner 默认 64KB token 上限,处理长行时调大
  3. Reader 比 Scanner 更灵活,但代码更复杂
  4. Writer 一定要记得 Flush,关键数据显式检查错误
  5. 批量处理时定期 Flush 可减少数据丢失风险
  6. 使用 ReuseRecord = true 优化 csv.Reader 的内存使用
  7. JSON 流式解析用 json.Decoder 而非一次性 json.Unmarshal

小结

bufio 是 Go 文本处理的基石包。Scanner 适合逐行读取,代码简洁;Reader 适合更灵活的读取控制;Writer 适合缓冲输出,减少系统调用。处理大文件时,流式处理比一次读入内存更可靠。

在 HTTP 响应读取、CSV 解析、JSON 流式处理等场景中,合理配置 bufio 的缓冲策略,可以显著提升程序性能。入门阶段可以记住一个判断原则:配置文件和小数据用 os.ReadFile 很方便;日志、导出文件、长文本和流式输入,用 bufio.ScannerReader 更稳。工具选对了,程序在数据变大时才不会突然撑爆内存。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 熔断、降级与限流:Go 微服务韧性设计完全指南
  2. 事件溯源与 CQRS 在 Go 中的实践:复杂业务系统的架构升级
  3. TinyGo 嵌入式开发与物联网实战:微控制器编程完全指南