Go bufio 入门:逐行读取、缓冲写入和处理大文件的基本功

本文讲解 Go bufio 包中的 Scanner、Reader、Writer 和 Flush,帮助初学者处理逐行读取、大文件统计和缓冲输出。

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

入门时我们常用:

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 != ""

ReaderScanner 稍啰嗦,但控制力更强。简单逐行统计优先用 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.ScannerReader 更稳。工具选对了,程序在数据变大时才不会突然撑爆内存。

继续阅读

探索更多技术文章

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

全部文章 返回首页