Go bufio.Scanner 兼容单独 CR 换行的 SplitFunc 方案
如果输入文本只使用 \n 或常见的 \r\n,bufio.Scanner 默认的 bufio.ScanLines 通常就够用。但有些旧日志、跨平台导出文件或拼接文本会混用三种行尾:LF、CRLF,以及单独的 CR。
这时可以给 Scanner 换一个自定义 SplitFunc:优先处理 CRLF,再处理 LF,最后把不跟随 LF 的 CR 当作独立行结束符。关键是遇到位于缓冲区最后的 CR 时不能急着返回,要等下一批数据确认它是不是 CRLF。

默认 ScanLines 为什么漏掉单独 CR
Scanner 的默认拆分函数是 ScanLines。它以换行字节 \n 作为主要分隔点;如果分隔点前面紧挨着一个 \r,会把这个回车一起从 token 尾部去掉。因此,\r\n 会被当作一个完整的 Windows 行尾。
但单独的 \r 后面没有 \n,它不会触发默认的换行判断。比如输入 甲\r乙\n,默认函数可能把 甲\r乙 作为一行交给调用方,而不是得到两行。这个行为并不是 Scanner 读取失败,而是默认的分隔规则没有覆盖你的数据格式。
另外,SplitFunc 不只是返回 token,它还要返回已经消费的字节数 advance 和错误值。返回 (0, nil, nil) 表示当前数据还不足以组成一个完整 token,Scanner 应该继续读取;因此,判断缓冲区边界和 atEOF 是实现的重点。
按 CRLF、LF、CR 的顺序设计 SplitFunc
实现时先扫描当前缓冲区,遇到 \n 时再回看前一个字节是否为 \r。这样可以把 CRLF 一次性消费掉,并从 token 中去掉 CR。遇到 \r 时,如果下一个字节不是 \n,就可以把它视为单独行尾;如果它已经位于缓冲区末尾,则暂时等待下一批数据。
package main
import "bufio"
// scanMixedLines 同时接受 LF、CRLF 和单独 CR 三种行尾。
func scanMixedLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
for i, b := range data {
switch b {
case '\n':
// LF 前面的 CR 属于 CRLF,不能把它留在当前 token 中。
end := i
if i > 0 && data[i-1] == '\r' {
end = i - 1
}
return i + 1, data[:end], nil
case '\r':
if i+1
这里有两个容易写错的点。第一,CRLF 分支不能在看到 CR 时立即返回,否则下一次又会处理 LF,可能产生空 token 或多推进一次。第二,advance 必须覆盖已经消费的分隔符,返回的 token 则只保留行内容。
处理缓冲区末尾与 atEOF
Scanner 会分批把数据交给 SplitFunc。假设第一批数据正好以 \r 结尾,此时无法知道下一批开头是不是 \n。如果立刻把它判为单独 CR,跨缓冲区的 CRLF 就会被错误拆开。
因此,代码在 CR 位于当前数据最后一个字节且 atEOF=false 时返回 (0, nil, nil)。Scanner 会保留未处理数据并继续读入。只有当 atEOF=true,才能确定这个末尾 CR 不会再接上 LF,可以安全地结束当前行。

同理,文件结尾没有换行符时,atEOF=true 且 data 仍有内容,应该返回这段剩余文本作为最后一个 token。只有 atEOF=true 且没有剩余字节时,才返回空结果结束扫描。
接入 Scanner 并检查 Err
定义好拆分函数后,用 Scanner.Split 替换默认规则即可。示例输入故意混合三种行尾,并在循环后检查 Err,这样底层 Reader 的错误不会被误认为正常结束。
package main
import (
"bufio"
"fmt"
"strings"
)
func main() {
// 输入同时包含单独 CR、LF、CRLF,以及没有终止符的最后一行。
input := "甲\r乙\n丙\r\n丁"
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(scanMixedLines)
for scanner.Scan() {
// Text 会把当前 token 转成字符串,适合本例的短文本输出。
fmt.Printf("行:%s\n", scanner.Text())
}
if err := scanner.Err(); err != nil {
// Scan 返回 false 既可能是 EOF,也可能是底层读取错误。
fmt.Printf("扫描失败:%v\n", err)
}
}
预期的行内容是 甲、乙、丙、丁。这里的输出是代码逻辑的示例结果,不代表图片中的内容是终端截图;文章配图只是帮助理解拆分关系的静态说明图。
长行和复用场景要单独处理
自定义 SplitFunc 只解决分隔符问题,不会改变 Scanner 的 token 大小限制。默认缓冲上限适合普通文本,如果单行可能很长,需要在第一次调用 Scan 前设置更大的缓冲区:
// 为可能超过默认上限的单行设置初始缓冲区和最大 token 大小。 scanner.Buffer(make([]byte, 32*1024), 1024*1024) scanner.Split(scanMixedLines)
如果输入可能出现不可控长度的单行,或者需要更细的读取错误、重试、回退和顺序扫描控制,应该考虑 bufio.Reader。Scanner 的优势是把常见的 token 循环写得简洁;它并不适合所有需要精确掌握 Reader 状态的场景。
常见误区总结
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 单独 CR 没有换行 | 默认 ScanLines 主要寻找 LF | 使用自定义 SplitFunc |
| 跨缓冲区的 CRLF 被拆开 | CR 在缓冲区末尾时立即返回 | 返回 (0, nil, nil),等待更多数据 |
| 最后一行消失 | 只处理分隔符,忽略 atEOF 剩余文本 | atEOF 且 data 非空时返回最后 token |
| 扫描停止但没有错误信息 | 没有在循环后检查 Scanner.Err | Scan 循环结束后读取 Err |
结语
这类问题的核心不是把 ScanLines 改成更复杂的字符串替换,而是正确实现 SplitFunc 的三件事:识别行尾、返回准确的推进字节数、在数据不足时尊重 atEOF。按 CRLF、LF、单独 CR 的顺序判断,再覆盖无终止符的最后一行,就能稳定读取混合格式文本。
如果你的数据只有 LF 或 CRLF,继续使用默认 bufio.ScanLines 更简单;只有在输入格式确实包含单独 CR,或你需要自定义 token 边界时,才值得引入这个 SplitFunc。
相关问题
- SplitFunc 为什么要返回 (0, nil, nil)?因为当前缓冲区还没有完整 token,Scanner 需要保留数据并继续读取。
- CRLF 会不会被拆成两行?只要先识别 LF 并回看前一个 CR,再一次性推进到 LF 后面,就不会拆开。
- 没有换行符的最后一行怎么办?在
atEOF=true且data非空时返回剩余数据。 - Scanner 报 token too long 怎么办?先用
Scanner.Buffer提高上限;如果长度不可控,再评估bufio.Reader。
官方参考:https://pkg.go.dev/bufio#ScanLines、https://go.dev/src/bufio/scan.go
Go bufio.Scanner 升级后行为变化怎么定位
- 上一篇
- Go bufio.Scanner 升级后行为变化怎么定位
- 下一篇
- Go strconv.ParseInt 返回错误时怎么区分原因
-
- Golang · Go问答 | 43分钟前 | 标准库 · Go问答 · 版本升级 · Go Base strconv.ParseInt Go升级 bitSize NumError ErrRange ErrSyntax ParseInt行为
- Go strconv.ParseInt 升级后行为变化怎么定位
- 401浏览 收藏
-
- Golang · Go问答 | 50分钟前 |
- Go strconv.ParseInt 资源没有释放为什么
- 150浏览 收藏
-
- Golang · Go问答 | 51分钟前 | 标准库 · 错误处理 · Go问答 · Go Base strconv.ParseInt bitSize ParseInt结果 NumError ErrRange ErrSyntax
- Go strconv.ParseInt 结果不稳定怎么办
- 273浏览 收藏
-
- Golang · Go问答 | 52分钟前 |
- Go strconv.ParseInt 返回错误时怎么区分原因
- 398浏览 收藏
-
- Golang · Go问答 | 56分钟前 | go · bufio · 排错 · 兼容性 · bufio.Scanner Go升级排查 Go扫描器行为
- Go bufio.Scanner 升级后行为变化怎么定位
- 262浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · 文件读取 · bufio · 资源管理 · 文件句柄 bufio.Scanner Go资源释放
- Go bufio.Scanner 资源没有释放为什么
- 147浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go bufio.Scanner 结果不稳定怎么办
- 201浏览 收藏
-
- Golang · Go问答 | 1小时前 | 标准库 · 错误处理 · 文件读取 · Go问答 · 排错 · Go bufio.Scanner errors.Is errors.As ErrTooLong Scanner.Err 文本读取
- Go bufio.Scanner 返回错误时怎么区分原因
- 428浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go os.Lstat 升级后行为变化怎么定位
- 361浏览 收藏
-
- Golang · Go问答 | 1小时前 | go ·
- Go os.Lstat 配置没有生效怎么办
- 206浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go os.Lstat 资源没有释放为什么
- 430浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 867次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 868次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 948次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 925次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 762次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览
