登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go bufio.Scanner 处理空行与扫描错误

来源:17golang原创

时间:2026-10-31 11:36:04 329浏览 收藏

用 bufio.Scanner 按行读取文本时,空行不是“没有读到数据”:默认的 bufio.ScanLines 会把它作为一个长度为 0 的 token 返回。真正的扫描错误也不会直接从 Scan() 返回错误,而是要在循环结束后调用 Err() 判断。

因此,稳定的写法是先决定空行是否有业务意义,再在 for scanner.Scan() 结束后检查 scanner.Err()。如果输入可能包含很长的一行,还要在第一次调用 Scan() 之前配置 Buffer,否则默认缓冲区可能导致扫描提前停止。

官方文档:https://pkg.go.dev/bufio

先确认空行到底会返回什么

NewScanner 默认使用 ScanLines。它会去掉行尾的换行符,但不会把中间的空行自动删除,所以输入 alpha\n\n beta 时,三次扫描得到的 token 依次是 alpha、空字符串和 beta。空行要不要忽略,是调用方的选择。

Go bufio Scanner 按行返回非空行和空行 token 的关系结构图
图1:ScanLines、Scan 和 Text 的关系说明图;空行也会形成一个长度为零的 token,不是运行截图。

如果空行代表记录之间的分隔符,应该保留它;如果只是日志中的视觉留白,可以在拿到 token 后再跳过。不要用“扫描不到文本就结束”的判断替代 Scan(),因为这会把合法空行和真正的输入结束混在一起。

package main

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

func main() {
	// 中间包含一个空行,用来观察 ScanLines 的默认行为。
	input := "alpha\n\n beta\n"
	scanner := bufio.NewScanner(strings.NewReader(input))

	for scanner.Scan() {
		line := scanner.Text()
		// 空行仍然是一次成功扫描;是否跳过由业务规则决定。
		if line == "" {
			fmt.Println("发现空行")
			continue
		}
		fmt.Printf("保留内容:%q\n", line)
	}
	// Scan 返回 false 后再检查错误,正常 EOF 不会被当成错误。
	if err := scanner.Err(); err != nil {
		fmt.Printf("扫描失败:%v\n", err)
	}
}

这里用 Text() 把最近一次 token 转成字符串,适合需要长期保存或继续处理的文本。若只需要短暂查看字节,可以使用 Bytes(),但它指向的底层数组可能在下一次 Scan() 后被覆盖。

把空行策略写成明确的业务分支

常见的旧代码会直接把每一行追加到切片,结果把配置文件中的空行也当成有效记录;另一种代码使用 strings.TrimSpace 后再判断,这又会把只含空格或制表符的行一并删除。两种行为都可能是对的,关键是不要无意中改变输入语义。

需要“只跳过真正的空行”时,用 line == "";需要“忽略只含空白字符的行”时,才使用 strings.TrimSpace(line) == ""。如果缩进本身有意义,不要在保存前无条件 Trim。

func collectRecords(scanner *bufio.Scanner, ignoreBlankOnly bool) ([]string, error) {
	var records []string
	for scanner.Scan() {
		line := scanner.Text()
		// 只在业务明确要求时跳过空白行,避免误删缩进信息。
		if ignoreBlankOnly && strings.TrimSpace(line) == "" {
			continue
		}
		records = append(records, line)
	}
	// 循环结束后统一返回扫描错误,调用方可以决定是否重试或终止。
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return records, nil
}

扫描结束后一定要检查 Err

Scan() 的返回值只有一个布尔值:true 表示拿到了下一个 token,false 表示没有下一个 token。它既可能是正常读到 EOF,也可能是底层 Reader 返回错误,或者 token 超过了当前允许的最大长度。因此只写循环而不检查 Err(),就会把“读取了一半”误认为“文件完整读完”。

Go bufio Scanner 空行路径与扫描错误路径的边界结构图
图2:空行 token、扫描结束和 Err 判定的边界说明图;这是静态结构图,不是实际运行结果。
func readLines(r io.Reader) ([]string, error) {
	scanner := bufio.NewScanner(r)
	var lines []string
	for scanner.Scan() {
		// Text 只取当前 token;需要保留空行时不要过滤空字符串。
		lines = append(lines, scanner.Text())
	}
	// Err 会忽略 io.EOF,但会返回其他读取错误或 token 过长错误。
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return lines, nil
}

如果输入来自文件,文件本身仍然要由调用方负责关闭;Scanner 不会替你关闭传入的 io.Reader。推荐在打开文件成功后立即注册 defer Close(),并把扫描错误与关闭错误按项目规范处理。

长行导致扫描失败时配置 Buffer

Scanner 默认有内部 token 缓冲上限。遇到单行 JSON、堆栈信息或导出字段特别长的输入时,Scan() 可能返回 false,随后 Err() 报告 token 过长。此时应在第一次调用 Scan() 前调用 Buffer。

func scanLongLines(r io.Reader) error {
	scanner := bufio.NewScanner(r)
	// 初始缓冲区减少小幅扩容;最大值要覆盖业务允许的单行长度。
	scanner.Buffer(make([]byte, 64*1024), 2*1024*1024)

	for scanner.Scan() {
		// 这里只处理当前行,避免把所有内容一次性堆进内存。
		line := scanner.Text()
		_ = line
	}
	// Buffer 不能替代错误检查,超出最大值仍会从 Err 暴露出来。
	return scanner.Err()
}

Buffer 的第二个参数是允许分配的最大 token 尺寸,不是“文件最大尺寸”。即使把它调大,也不代表适合用 Scanner 处理无界输入;如果必须控制读取边界、恢复读取过程或顺序执行多轮扫描,应评估 bufio.Reader 等更可控的方案。

按这份清单迁移旧的按行读取代码

检查项迁移后的明确行为
空行明确保留,或在 token 已返回后按规则跳过
只含空格的行只有业务允许时才使用 TrimSpace 判断
扫描结束循环后调用 Err,不能只看 Scan 返回 false
超长 token在第一次 Scan 前设置 Buffer,并限制最大值
文件资源由打开文件的一方负责 Close,Scanner 不接管生命周期
末尾无换行ScanLines 仍会返回最后一个非空行

回归测试至少覆盖:连续两个换行符、只有空白字符的行、最后一行没有换行符、Reader 返回错误,以及超过 Buffer 上限的单行。这样才能确认“空行处理”没有掩盖“输入未完整读取”的问题。

常见问题

空行会让 Scanner 提前结束吗?

不会。默认 ScanLines 会返回空 token,下一次扫描仍会继续读取后面的内容。

为什么 Scan 返回 false 但 Err 是 nil?

通常是正常到达 EOF。Err() 只报告非 EOF 错误,所以仍然应该调用它来完成判断。

调用 Buffer 后还是报 token too long 怎么办?

确认 Buffer 在第一次 Scan() 前调用,并检查第二个参数是否覆盖真实单行长度;如果输入长度没有可靠上限,应重新评估读取器选择。

Scanner 会自动关闭文件吗?

不会。传入的 Reader 生命周期仍由调用方管理,文件读取完成后要按原有责任链关闭。

把空行、EOF、读取错误和超长 token 分成四种状态后,bufio.Scanner 的行为就比较稳定:循环负责消费 token,Err() 负责揭示失败原因,Buffer 负责把明确的单行上限传达给运行时。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>