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

bufio.Scanner token too long 如何改用 Reader

来源:17golang原创

时间:2026-11-13 09:57:21 196浏览 收藏

遇到 bufio.Scanner: token too long,通常不是文件坏了,而是 Scanner 的分词模型不适合当前输入。它默认按 token 缓冲,标准库文档给出的默认最大扫描尺寸是 64 KiB,而且实际可用长度还要给分隔符留空间。单行日志、压缩后的 JSON 或导入字段超过这个边界时,继续调业务逻辑没有意义,应该改用 bufio.Reader。

要点速览
  • 需要完整的一行:优先用 ReadString('\n') 或 ReadBytes('\n')。
  • 行长不可控:用 ReadLine 识别 isPrefix,分段消费或拼接。
  • 最后一段没有换行并不等于异常;只有错误不是 io.EOF 时才应失败。
碰到bufio.Scanner返回token too long报错的时候,你可以直接切换用io.Reader相关方法读取内容,不需要硬改Scanner的缓冲区大小,适配超大行文本场景会更灵活。
你可以优先用Reader的Read方法分块读全量内容,也可以配合bufio.Reader的ReadLine、ReadBytes方法替代Scanner的扫描逻辑,完全避开Scanner内置64KB单token长度限制的问题。

先定位 Scanner 的 token 上限与错误边界

Scanner.Scan 返回 false 后,要先读取 Err。如果得到 token too long,说明当前 split 函数识别到的一个 token 放不进 Scanner 的缓冲区。默认的 ScanLines 把一行当成 token,所以一行特别长就会触发这个限制;底层文件仍可能可以继续读取。

scanner := bufio.NewScanner(r)
for scanner.Scan() {
    // 这里处理一行;短输入时 Scanner 的代码更简洁。
    handle(scanner.Text())
}
if err := scanner.Err(); err != nil {
    // token too long 与底层读取错误都从这里暴露。
    return fmt.Errorf("scan input: %w", err)
}

如果业务确实知道单行上限,也可以在第一次 Scan 前调用 Scanner.Buffer 放大上限。但这仍然把整行装入 token,无法解决真正的无界输入;标题中的改用 Reader,重点就是把“分隔符读取”和“token 大小限制”拆开。

Go bufio.Scanner token too long 与 bufio.Reader 按换行分隔读取的结构说明图
图1:结构说明图,展示 Scanner 的 token 缓冲边界与 Reader 按分隔符消费输入的关系。

用 Reader.ReadString 按分隔符读取完整内容

需要得到完整字符串、并且单行大小在业务可接受范围内时,ReadString 最直接。它会持续读取到指定分隔符;返回值可能同时带着内容和错误,所以最后一段没有换行时,要先处理非空内容,再判断错误是否只是 io.EOF。

func readLines(r io.Reader, handle func(string) error) error {
    br := bufio.NewReader(r)
    for {
        line, err := br.ReadString('\n')
        if len(line) > 0 {
            // 去掉行尾换行;最后一行没有换行时仍保留其内容。
            line = strings.TrimSuffix(line, "\n")
            line = strings.TrimSuffix(line, "\r")
            if callbackErr := handle(line); callbackErr != nil {
                // 回调只执行一次,避免重试副作用。
                return fmt.Errorf("handle line: %w", callbackErr)
            }
        }
        if errors.Is(err, io.EOF) {
            // EOF 只代表输入自然结束,不代表最后一行丢失。
            return nil
        }
        if err != nil {
            return fmt.Errorf("read line: %w", err)
        }
    }
}

上例的回调应只调用一次并保存返回值,生产代码可把它改成 callbackErr := handle(line),避免失败时重复执行副作用。若不想把字节转成字符串,ReadBytes('\n') 使用同样的 EOF 判断,但返回的是 []byte。

用 Reader.ReadLine 分段处理超长行

当单行可能达到几十 MiB,直接用 ReadString 会产生较大的瞬时内存。ReadLine 返回 line、isPrefix 和错误:isPrefix=true 表示这一段还没有遇到完整换行。可以把片段交给增量解析器,也可以只在确实需要完整字符串时用 bytes.Buffer 拼起来。

func readLongLines(r io.Reader, handle func([]byte) error) error {
    br := bufio.NewReader(r)
    var full bytes.Buffer
    for {
        part, prefix, err := br.ReadLine()
        if err != nil {
            if errors.Is(err, io.EOF) && full.Len() == 0 {
                return nil
            }
            return fmt.Errorf("read long line: %w", err)
        }
        // Copy 片段,避免后续读取覆盖 Reader 内部缓冲区中的字节。
        full.Write(part)
        if prefix {
            // 还未到换行,继续读取同一行的下一段。
            continue
        }
        if err := handle(full.Bytes()); err != nil {
            return fmt.Errorf("handle long line: %w", err)
        }
        full.Reset()
    }
}

ReadLine 的片段来自 Reader 的内部缓冲,不能把返回的切片长期保存后再继续读;需要跨轮次保留时必须复制。若输入来自不可信上传,拼接前还应自行设置总长度上限,超过上限立即丢弃当前记录并返回错误。

Go bufio.Reader ReadLine 的 isPrefix 分段与 bytes.Buffer 拼接边界说明图
图2:结构说明图,展示 ReadLine 分段、isPrefix 状态和完整行缓冲之间的边界。

按输入边界选择 API 并补上回归检查

场景建议 API关键判断
普通短行、需要完整字符串ReadString处理 EOF 携带的最后一段
需要字节切片或自定义分隔符ReadBytes确认切片生命周期和内存峰值
行长不可控、可增量处理ReadLineisPrefix 为 true 时不可提前提交
长度明确且始终较小Scanner.Buffer设置上限,不把它当成无界读取

迁移后至少覆盖三组输入:恰好带换行的长行、文件末尾没有换行的长行、分成多个 Reader 缓冲片段的长行。再加一组底层 Reader 主动返回错误,确认错误不会被 io.EOF 分支吞掉。这样改动的核心不是把 64 KiB 改成更大的数字,而是让读取策略匹配输入边界。

常见问题

只把 Scanner.Buffer 调到 10 MiB 可以吗?

可以用于有明确上限的受控输入,但它仍会把一个 token 放进内存;对无界或不可信单行,Reader 分段读取更稳妥。

ReadString 返回内容和 io.EOF 时要丢掉内容吗?

不要。只要返回内容非空,应先处理最后一段;io.EOF 只表示后面没有更多字节。

ReadLine 的 isPrefix 为 true 能直接交给业务吗?

只有业务明确支持分片协议时才可以。按行解析通常要继续收集片段,直到 isPrefix=false 再提交一条记录。

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