Go bufio.Scanner 返回错误时怎么区分原因
来源:17golang原创
时间:2026-11-03 20:37:57 428浏览 收藏
用 bufio.Scanner 读取文本时,Scan() 返回 false 只说明扫描结束了,不能直接等同于“发生了错误”。正常读到 io.EOF 时,Scanner.Err() 会返回 nil;只有底层读取失败、分词函数主动报错、token 超过缓冲区上限等情况,才需要继续区分原因。
官方文档:https://pkg.go.dev/bufio#Scanner
实际排查时可以固定成一条规则:先在循环结束后保存 err := scanner.Err(),再用 errors.Is 匹配稳定的错误语义,用 errors.As 提取包含操作名和路径的结构化错误,不要依赖 err.Error() 的完整字符串。
先把 Scan 返回 false 和真实错误分开
Scanner 的典型写法是循环调用 Scan(),循环结束后再调用一次 Err()。Err() 的约定是忽略 io.EOF,返回第一个非 EOF 错误。因此,下面的判断已经覆盖了“文件正常读完”和“扫描过程中出现错误”两条主路径。
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
func readLines(input string) ([]string, error) {
scanner := bufio.NewScanner(strings.NewReader(input))
var lines []string
for scanner.Scan() {
// Text 返回当前 token;这里复制成 string,避免依赖 Scanner 的复用缓冲区。
lines = append(lines, scanner.Text())
}
// Err 会把正常的 io.EOF 转换成 nil,所以 nil 不代表遗漏了 EOF。
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("扫描文本失败: %w", err)
}
return lines, nil
}
func main() {
lines, err := readLines("alpha\nbeta\n")
if err != nil {
// 这里保留包装上下文,调用方仍可继续用 errors.Is/As 分类。
panic(err)
}
fmt.Println(len(lines), io.EOF == nil)
}
这段代码的关键不是把错误打印出来,而是让错误继续沿着 %w 形成可检查的错误链。若 Err() 返回 nil,通常可以认为输入已经正常结束;若返回非 nil,再进入下面的来源分类。

Scanner 的错误主要来自三条路径
Scanner 自己不会把所有错误重新包装成一种类型。它会记录扫描过程中遇到的第一个错误,因此分类时先问“错误是从哪里产生的”,比先看错误字符串更可靠。
- 底层 Reader 错误:
io.Reader.Read返回了非 EOF 错误,例如权限、设备或网络读取失败。Scanner 会把它保留下来。 - SplitFunc 错误:自定义分词函数返回非
nil错误,扫描会停止;如果返回的是bufio.ErrFinalToken,那是主动结束,不会被当作普通错误返回。 - token 太大或实现异常:默认 token 缓冲上限约为 64 KiB,超过限制会得到
bufio.ErrTooLong;自定义 SplitFunc 返回负的 advance 或越过输入,也会得到 Scanner 定义的错误。
还有一个容易误判的边界:Scan() 返回 false 后,最后一段没有换行的非空文本仍可能已经作为 token 返回。不要因为输入末尾没有换行,就把正常结束当成异常。
用 errors.Is 匹配稳定的错误语义
如果错误经过 fmt.Errorf("...: %w", err) 包装,直接使用 err == target 可能失效。errors.Is 会沿着错误链寻找目标,适合判断 io.ErrUnexpectedEOF、bufio.ErrTooLong 或自定义哨兵。
package main
import (
"bufio"
"errors"
"fmt"
"io"
)
func scannerReason(err error) string {
if err == nil {
// nil 表示正常结束,Scanner 已经隐藏了 io.EOF。
return "正常结束"
}
if errors.Is(err, bufio.ErrTooLong) {
// 先识别 Scanner 自身的稳定哨兵,提示调用方调整输入或缓冲区。
return "单个 token 超过缓冲区上限"
}
if errors.Is(err, io.ErrUnexpectedEOF) {
// 错误可能被上层添加了上下文,Is 仍能识别原始语义。
return "输入提前结束"
}
return "其他读取或分词错误"
}
func main() {
wrapped := fmt.Errorf("读取记录: %w", io.ErrUnexpectedEOF)
fmt.Println(scannerReason(wrapped))
}
判断顺序可以按业务影响调整。比如超长 token 需要提示“增大 Buffer 或改用 Reader”,而网络读取失败可能需要重试;分类函数只负责识别,不要把重试、降级和日志策略全部塞进其中。
用 errors.As 提取 PathError 等结构化信息
有些错误不仅需要知道“是哪一类”,还需要拿到操作名和路径。io/fs.PathError 包含 Op、Path 和底层 Err 字段,适合用 errors.As 提取。它同样能够穿过包装层,所以不用对 err 做一次次类型断言。
package main
import (
"errors"
"fmt"
"io/fs"
)
func describeReadError(err error) string {
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
// PathError 提供了可用于日志和提示的操作名、路径与根因。
return fmt.Sprintf("操作=%s,路径=%s,根因=%v", pathErr.Op, pathErr.Path, pathErr.Err)
}
return fmt.Sprintf("未识别的读取错误:%v", err)
}
func main() {
err := fmt.Errorf("加载配置: %w", &fs.PathError{
Op: "read",
Path: "config/app.conf",
Err: fs.ErrPermission,
})
fmt.Println(describeReadError(err))
}
如果同时需要判断根因,可以先用 errors.As 取得 PathError,再对 pathErr.Err 或原始错误链使用 errors.Is。不要用字符串包含“permission”来代替权限错误判断,因为不同平台和包装层的文案可能变化。

遇到 ErrTooLong 时先看 Buffer 是否设置得太晚
Scanner 默认适合简单的、按行读取的文本。如果单行可能超过默认上限,应在第一次调用 Scan() 之前调用 Buffer。第二个参数是允许 Scanner 使用的最大 token 缓冲区大小;一旦扫描已经开始,再调用 Buffer 会触发 panic。
package main
import (
"bufio"
"fmt"
"strings"
)
func scanLargeLine(input string) error {
scanner := bufio.NewScanner(strings.NewReader(input))
// 在第一次 Scan 前设置上限;这里允许约 1 MiB 的单个 token。
scanner.Buffer(make([]byte, 32*1024), 1024*1024)
for scanner.Scan() {
// 只在当前迭代消费 token,避免把 Bytes 的复用切片长期保存。
fmt.Println(len(scanner.Bytes()))
}
if err := scanner.Err(); err != nil {
// ErrTooLong 可被调用方识别,其他错误则继续沿错误链上抛。
return fmt.Errorf("读取长行失败: %w", err)
}
return nil
}
如果输入本来就可能包含无界长度的记录,单纯把上限调得很大并不一定是好方案。Scanner 会在扫描停止时让 Reader 可能已经向前读取了超过最后一个 token 的数据;需要更细粒度控制、连续多轮扫描或处理超大记录时,应该考虑 bufio.Reader。
把错误分类收敛成排查清单
一个可复用的读取函数至少应该保留三层信息:是否正常结束、Scanner 产生的具体错误、错误链中可提取的上下文。实际接入日志或接口响应时,可以按下面的顺序处理:
- 循环结束后只调用一次
scanner.Err(),先判断是否为nil。 - 用
errors.Is(err, bufio.ErrTooLong)识别 token 超限,区分“输入太大”和“读取失败”。 - 用
errors.As(err, &pathErr)提取fs.PathError,记录Op和Path。 - 对自定义 SplitFunc 的错误使用稳定哨兵或自定义类型,不要只比较展示文本。
- 如果需要重试、降级或返回 HTTP 状态码,在 Scanner 外层决定,避免分类函数和业务动作耦合。
总结来说,Scan() == false 是控制流信号,Err() == nil 才表示没有需要上报的扫描错误;errors.Is 解决“是不是某个稳定语义”,errors.As 解决“能否取出某类错误的字段”。先按这两个维度分类,再决定是调大 Buffer、修复 Reader,还是修正 SplitFunc,排查路径会清晰很多。
相关问题
Scanner.Err() 为什么不返回 io.EOF? 因为 EOF 是正常扫描结束的信号,Scanner 的约定会将其转换成 nil。
能不能直接比较 err.Error()? 不建议。字符串是展示信息,包装层、平台和版本都可能改变文案,应优先使用 errors.Is 或 errors.As。
什么时候不适合继续使用 Scanner? 当 token 可能非常大、必须精确控制 Reader 消耗位置,或需要在同一个 Reader 上进行复杂的连续读取时,优先评估 bufio.Reader。
参考资料
https://pkg.go.dev/bufio#Scanner
https://go.dev/src/bufio/scan.go
https://pkg.go.dev/errors
https://pkg.go.dev/io/fs#PathError
-
369 收藏
-
344 收藏
-
278 收藏
-
464 收藏
-
327 收藏
-
150 收藏
-
Golang · Go问答 | 20分钟前 | 标准库 · 错误处理 · Go问答 · Go Base strconv.ParseInt bitSize ParseInt结果 NumError ErrRange ErrSyntax273 收藏
-
398 收藏
-
438 收藏
-
262 收藏
-
147 收藏
-
201 收藏
-
Golang · Go问答 | 1小时前 | 标准库 · go · 错误排查 · Go 符号链接 errors.Is errors.As PathError filepath.EvalSymlinks324 收藏
-
361 收藏
-
206 收藏
-
430 收藏
-
472 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习