Go bufio.Scanner 读取超长行怎么安全扩容
来源:17golang原创
时间:2026-09-08 13:54:45 100浏览 收藏
用 bufio.Scanner 按行读日志时,短行一切正常,换成一条几十万字节的 JSON 日志却可能直接停止,并在 Scanner.Err() 中看到 bufio.Scanner: token too long。安全的处理方式不是把限制改成一个特别大的数字,而是在第一次 Scan() 前用 Buffer 设定“初始容量 + 最大 token 大小”,再根据业务允许的单行字节数决定是否继续使用 Scanner。
- Scanner 默认最大 token 约为 64 KiB,超长时 Scan 返回 false,必须检查 Err。
- Buffer 只能在第一次 Scan 前调用,max 要覆盖内容和换行符,并保留业务上限。
- 行长不可控、需要分段或要从超长行中恢复时,优先比较 bufio.Reader。
先把 Scanner 的上限和内存边界分开看
NewScanner 默认使用 ScanLines,它把一行作为一个 token 返回。默认缓冲上限是 64 KiB,而且真实可容纳的 token 可能略小,因为输入还需要放下换行符。超出边界后,扫描会不可恢复地停止;这不是“少读了一段”,而是当前 Scanner 已经不能继续完成这次按行扫描。
可以把边界理解成一条静态关系:输入流交给 Scanner,Scanner 通过 ScanLines 找行,token 先放进初始缓冲区;如果不够,缓冲区在最大 token 上限内扩容,超过上限才会落到 ErrTooLong。下面的图只展示这些组件之间的关系,便于先确定应该调哪一个参数。

用 Buffer 在第一次 Scan 前设置可控上限
最小改动是在创建 Scanner 后立即调用 Buffer。初始容量决定一开始准备多少空间,max 决定扫描期间允许申请到多大;二者不是同一个概念。示例把业务单行限制设为 2 MiB,并为 LF 或 CRLF 留出少量余量:
package main
import (
"bufio"
"fmt"
"io"
)
func readLines(r io.Reader) error {
scanner := bufio.NewScanner(r)
// 初始空间保持适中,最大值按业务允许的单行字节数设置。
const maxLineBytes = 2 * 1024 * 1024
// ScanLines 还需要容纳行尾分隔符,留出两个字节余量。
scanner.Buffer(make([]byte, 64*1024), maxLineBytes+2)
for scanner.Scan() {
line := scanner.Text()
// 这里处理一整行;不要把超大行无条件长期放入内存缓存。
_ = line
}
// Scan 返回 false 既可能是 EOF,也可能是 token 太长或底层读错误。
if err := scanner.Err(); err != nil {
return fmt.Errorf("读取文本行失败: %w", err)
}
return nil
}
顺序很重要:Buffer 在扫描开始后调用会 panic。循环结束也不能只看 Scan() 的 false,必须读取 Err();这样 EOF 会被正常区分,超长 token 和底层 I/O 错误也不会被悄悄吞掉。
超长行不是越大越好:按字节预算控制内存
max 应该来自业务边界,而不是拍一个很大的数。日志行、单条 JSON 消息和导入记录可以分别设定上限;超过上限时返回明确错误,让调用方丢弃、转存或改走专门的大记录通道。中文字符数和字节数也不能混用,UTF-8 文本的内存预算应按字节估算。
| 场景 | 建议 | 原因 |
|---|---|---|
| 稳定的小型日志 | 保留默认 Scanner | 边界清晰,代码简单 |
| 已知最大行长 | Buffer(适中初始值, 上限+分隔符余量) | 突破默认值且限制内存 |
| 长度不可控或需恢复 | 比较 bufio.Reader | 可以分段读取并自行处理边界 |
还有一个容易忽略的边界:Scanner.Bytes() 返回的底层数组可能在下一次 Scan 时被覆盖;如果要把当前行交给异步任务或跨循环保存,应复制它,或者直接使用 Text() 接受字符串分配。
什么时候应该换用 bufio.Reader
Scanner 适合“每条记录有明确上限、超限就失败”的输入。若一行可能非常长,或者需要读到分隔符后再决定如何处理,bufio.Reader 更容易控制:可以使用 ReadString 或 ReadBytes,也可以用 ReadLine 分段接收并通过前缀信息拼接。代价是调用方要自己处理片段、EOF 和单行累计大小。
选择关系可以概括为:Scanner 连接 Scanner.Buffer 与固定 token 上限,Reader 则把读取拆成 ReadString、ReadLine 等更细的读取接口;ErrTooLong 是 Scanner 的失败边界,不应把它当成可从中间恢复的信号。

常见问题
Buffer 的 max 是字符数还是字节数?
它对应字节缓冲区大小,应按 UTF-8 输入的字节预算估算,不要直接把“字符数”当成 max。
把 max 设置成 int 的最大值可以吗?
不建议。这样失去输入保护,异常大行可能带来内存压力;应根据日志、消息或记录的业务上限设置。
Scanner 扫描结束后为什么一定要检查 Err?
因为 Scan 返回 false 既可能代表正常 EOF,也可能代表 token 太长或底层读取错误;Err 才能区分这些情况。
-
328 收藏
-
Golang · Go教程 | 1小时前 | Go教程 · database/sql · 数据库元数据 · SQL NULL · Go database/sql rows ColumnTypes ColumnType.Length128 收藏
-
319 收藏
-
272 收藏
-
327 收藏
-
255 收藏
-
285 收藏
-
212 收藏
-
458 收藏
-
Golang · Go教程 | 3小时前 | Go教程 · 结构体标签 · encoding/xml · XML 序列化 · encoding/xml XMLName Go Marshal xml.Name XML 属性278 收藏
-
380 收藏
-
168 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习