当前位置:首页 > 文章列表 > Golang > Go问答 > Go bufio.Scanner 返回错误时怎么区分原因

Go bufio.Scanner 返回错误时怎么区分原因

来源:17golang原创 2026-11-03 20:37:57 0浏览 收藏

用 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,再进入下面的来源分类。

bufio.Scanner 正常 EOF、Reader 错误、SplitFunc 错误和缓冲区超限的结构关系
图1:bufio.Scanner 错误来源结构图,这是静态说明图,不是运行截图。

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”来代替权限错误判断,因为不同平台和包装层的文案可能变化。

scanner.Err 经过 errors.Is 哨兵匹配和 errors.As 类型提取的分类关系
图2:错误链分类关系图,这是静态说明图,不是运行截图。

遇到 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 产生的具体错误、错误链中可提取的上下文。实际接入日志或接口响应时,可以按下面的顺序处理:

  1. 循环结束后只调用一次 scanner.Err(),先判断是否为 nil。
  2. 用 errors.Is(err, bufio.ErrTooLong) 识别 token 超限,区分“输入太大”和“读取失败”。
  3. 用 errors.As(err, &pathErr) 提取 fs.PathError,记录 Op 和 Path。
  4. 对自定义 SplitFunc 的错误使用稳定哨兵或自定义类型,不要只比较展示文本。
  5. 如果需要重试、降级或返回 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

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go filepath.EvalSymlinks 返回错误时怎么区分原因Go filepath.EvalSymlinks 返回错误时怎么区分原因
上一篇
Go filepath.EvalSymlinks 返回错误时怎么区分原因
Go bufio.Scanner 结果不稳定怎么办
下一篇
Go bufio.Scanner 结果不稳定怎么办
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    867次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    868次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    948次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    925次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    762次使用