Go bufio.Scanner 如何按空行分隔多段文本
来源:17golang原创
时间:2026-10-16 10:56:35 392浏览 收藏
Go 的 bufio.Scanner 默认使用 ScanLines,每次 Scan() 取得一行文本;遇到空行时,Text() 得到空字符串。它不会自动把多行合并成段落,所以“按空行分隔多段文本”要在扫描循环外维护一个当前段缓冲区:普通行追加,空行提交,连续空行忽略,循环结束再提交一次最后一段。
官方文档:https://pkg.go.dev/bufio#Scanner
下面的写法适合配置文件片段、邮件正文、Markdown 简化解析等场景。本文只处理段落边界,不把超长 token 的缓冲修复混进来。
一、先明确 Scanner 返回什么
Scanner 负责从 io.Reader 取数据,分隔规则由 SplitFunc 决定。默认的 ScanLines 去掉行尾换行符,但仍然可以返回空行;因此段落并不是 Scanner 的内置结果,而是由业务代码根据空行重新聚合出来的。

二、用当前段缓冲区聚合普通行
最小实现是保留一个 current 切片。普通行直接追加,提交时用换行重新拼接。这样不会把每次 Scan 的结果误认为一个完整段落,也能保留段内原有的行结构。
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
// readParagraphs 按空行聚合段落,并把底层读取错误返回给调用方。
func readParagraphs(r io.Reader) ([]string, error) {
scanner := bufio.NewScanner(r)
// 显式写出默认规则,让段落判断建立在“逐行”语义上。
scanner.Split(bufio.ScanLines)
var paragraphs []string
var current []string
// flush 只提交非空段,连续空行不会制造空字符串段落。
flush := func() {
if len(current) == 0 {
return
}
paragraphs = append(paragraphs, strings.Join(current, "\n"))
current = current[:0]
}
for scanner.Scan() {
line := scanner.Text()
// 这里只判断空行,普通行原样保存,不改写正文空格。
if line == "" {
flush()
continue
}
current = append(current, line)
}
// 最后一段可能没有尾随空行,必须在 Scan 结束后补交。
flush()
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("扫描文本失败: %w", err)
}
return paragraphs, nil
}
这里的关键不是 strings.Join,而是 flush 的调用位置:空行触发一次,EOF 再触发一次。current[:0] 复用切片容量,但不会把已经追加到 paragraphs 的字符串内容清掉。
三、遇到空行时提交段落
空行提交的是“当前已经收集到的段”,不是空行本身。输入中有两个或更多连续空行时,第一次空行会提交当前段,后续空行只会命中空缓冲区并返回,因此结果不会凭空多出空段。
如果业务需要保留段落内部的换行,上面的 strings.Join(current, "\n") 就可以直接使用;如果只关心段落文本,也可以在提交时改用空格拼接,但那会改变原文的换行语义。
四、在 EOF 和 Err 分别收尾
Scan() 返回 false 既可能表示正常 EOF,也可能表示读取错误。先调用一次 flush 可以覆盖“最后一段没有空行结尾”的正常情况;随后检查 scanner.Err(),避免把半截输入误当成完整结果。

如果读取源在中途返回错误,函数应该保留错误信息并交给上层决定是否丢弃已收集的段落。示例选择返回 nil 和包装后的错误,表达“这次结果不完整”;如果业务允许部分结果,则可以把 paragraphs 与错误同时返回,但要在接口文档中写清楚。
五、按输入规则选择空行判断
严格空行与只包含空格或制表符的行不是同一个规则。示例使用 line == "",适合只有真正空行才分段的格式。如果上游文本把空白行也当作分隔符,可以把条件替换为 strings.TrimSpace(line) == "",但要接受它会把只含空格的正文行当成分隔线。
// isSeparator 根据输入协议决定“空行”的定义。
func isSeparator(line string, blankSpaceSeparates bool) bool {
if blankSpaceSeparates {
// 业务允许空格、制表符单独作为段落分隔。
return strings.TrimSpace(line) == ""
}
// 严格模式只接受长度为零的行。
return line == ""
}
选择规则时不要顺手调用 TrimSpace 改写每一行。更稳妥的做法是只用它参与分隔判断,真正追加到当前段的仍然保留原始 line。
常见误区
- 把每次
Scan()当成段落:默认规则返回的是行,不是段。 - 只在遇到空行时 flush:没有尾随空行的最后一段会丢失。
- 忽略
scanner.Err():读取失败时可能把不完整文本当成成功结果。 - 无条件
TrimSpace:会同时改变正文中的前后空格和分隔判断,应该分开处理。 - 把连续空行追加为结果:flush 前先判断当前缓冲区,才能保持段落结果紧凑。
速查表
| 输入情况 | 处理动作 | 结果 |
|---|---|---|
| 普通行 | 追加到 current | 继续收集当前段 |
| 严格空行 | flush current | 提交已有段落 |
| 连续空行 | 空 current 时跳过 | 不产生空段 |
| EOF | 最后再 flush 一次 | 保留无尾随空行的段落 |
| scanner.Err 非 nil | 返回错误 | 不把不完整输入当成功 |
相关问题
Scanner 能否直接按段落返回?可以自定义 SplitFunc,但段落规则和错误边界会一起进入切分函数;只需要按空行聚合时,先用 ScanLines 再在业务层 flush 更容易维护。
空行后还要保留分隔符吗?如果输出只需要段落文本,通常不需要;如果要重建原文,应额外记录分隔符数量,而不是把它们静默丢弃。
文本可能包含超长单行怎么办?那是 Scanner token 上限问题,应单独评估 Scanner.Buffer 或 bufio.Reader,不要和空行聚合规则混在一起。
-
120 收藏
-
168 收藏
-
233 收藏
-
364 收藏
-
Golang · Go教程 | 1天前 | 标准库 · 错误处理 · 文件读取 · Go教程 · Go bufio bufio.Reader ErrBufferFull ReadSlice bytes.Join 文本扫描472 收藏
-
419 收藏
-
372 收藏
-
255 收藏
-
471 收藏
-
499 收藏
-
348 收藏
-
477 收藏
-
311 收藏
-
206 收藏
-
462 收藏
-
154 收藏
-
305 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习