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

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

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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>