Go bufio.Scanner 如何自定义分隔符读取记录
来源:17golang原创
时间:2026-09-12 10:59:31 338浏览 收藏
如果输入不是按换行分隔,而是用 ||、@@ 这类业务标记分隔记录,Go 仍然可以继续使用 bufio.Scanner。做法是给 Scanner 注册一个自定义 SplitFunc:找到完整分隔符就返回一条 token,暂时找不到就等待更多字节,读到 EOF 再把末尾余量交出来。
Scanner.Split接收的是函数,不是单个字符;多字节分隔符可用bytes.Index查找。- 没有完整记录时必须返回
0, nil, nil;atEOF时要处理没有尾部标记的最后一条记录。 - 默认 token 缓冲上限是 64 KiB,长记录先用
Buffer调整,仍需要更强控制时考虑bufio.Reader。
自定义 SplitFunc 要同时处理分隔符和 EOF
Scanner 的默认切分规则是按行读取;Split 可以把它替换为业务规则。一个 SplitFunc 每次面对当前字节片段时,需要返回“应跳过多少字节”“本次 token 是什么”和“是否出错”三个结果。分隔符还没收完整时,不能急着把片段当成记录。

下面的函数支持多字节标记,并允许输入末尾没有 ||:
package main
import "bytes"
// splitByMark 按完整字节分隔符切分记录;分隔符本身不会进入 token。
func splitByMark(mark []byte) func([]byte, bool) (int, []byte, error) {
return func(data []byte, atEOF bool) (int, []byte, error) {
if i := bytes.Index(data, mark); i >= 0 {
// advance 必须越过整个分隔符,避免下一次扫描重复看到它。
return i + len(mark), data[:i], nil
}
if atEOF && len(data) > 0 {
// 最后一条记录没有结束标记时,仍然把剩余字节交给调用方。
return len(data), data, nil
}
// 分隔符可能被拆在两次读取之间,先保留当前数据等待补齐。
return 0, nil, nil
}
}
这里的 advance 是字节数,不是字符数,所以要写成 i + len(mark)。如果返回 0, nil, nil,Scanner 会继续向底层 Reader 请求数据;若直接返回当前片段,跨读取边界的分隔符就可能被误判。
给 Scanner 配置切分规则,再检查 Err
配置顺序很简单:先创建 Scanner,再设置 Split 和 Buffer,最后进入 Scan 循环。Buffer 必须在扫描开始前调用,max 表示单个 token 允许达到的最大缓冲范围。
package main
import (
"bufio"
"fmt"
"strings"
)
// readRecords 演示按 || 读取订单号,并在循环结束后区分 EOF 与真正错误。
func readRecords(input string) error {
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(splitByMark([]byte("||")))
// 允许单条记录最多约 1 MiB;初始缓冲只按需分配。
scanner.Buffer(make([]byte, 1024), 1024*1024)
for scanner.Scan() {
// Text 在本轮扫描内转成字符串;需要跨轮保存时应复制数据。
fmt.Printf("record=%q\n", scanner.Text())
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scan records: %w", err)
}
return nil
}
例如输入 order-1001||order-1002||order-1003,循环会得到三条记录,即使最后没有额外的 ||。Scanner 的正常结束不会把 EOF 当成错误;但底层读取失败或 token 超过上限,会在 Err() 中暴露。
空记录、token 生命周期和缓冲区上限不能混为一谈
连续分隔符会产生空 token。例如 A||||B 中间存在一条空记录。如果业务不允许空记录,要在循环里显式跳过或报错,不要在 SplitFunc 里悄悄改变记录语义。
Scanner.Bytes() 返回当前 token 的字节切片,下一次扫描后不要继续依赖它;需要放入队列、异步处理或长期保存时,复制一份。Scanner.Text() 适合直接得到字符串,但同样不应把“本轮 token”当成永久缓冲。
官方文档给出的默认最大 token 缓冲是 64 KiB,实际还可能需要容纳分隔符。可以用 Buffer 提高上限,但上限越大,异常输入占用的内存边界也越宽:
| 现象 | 处理方式 | 判断依据 |
|---|---|---|
| 记录短、边界固定 | 直接用 Scanner | 自定义 SplitFunc 足够表达规则 |
| 偶发长记录 | 先调用 Buffer | 可接受明确的单条记录上限 |
| 记录可能极长或需分段恢复 | 改用 bufio.Reader | 需要更强的错误控制和读取过程控制 |

什么时候不该继续扩大 Scanner 的 max
Buffer 解决的是“已知上限内的长 token”,不是无限长度输入的通用方案。如果输入来自不可信网络、单条记录可能持续增长,或者程序需要在超长数据中逐段恢复,继续把 max 调到很大只会把风险推迟。此时用 bufio.Reader.ReadString、ReadBytes 或更细粒度的读取逻辑,通常更容易控制内存和错误路径。
落地前可以按这份清单判断:分隔符是否可能跨读取边界;末尾无分隔符是否仍算有效记录;连续分隔符是否允许空记录;记录长度上限是多少;token 是否会交给异步任务;扫描结束后是否检查了 Err()。这些问题都明确后,自定义 Scanner 就会从一个小技巧变成稳定的输入边界。
常见问题
SplitFunc 能按中文字符串分隔吗?
可以。Scanner 面向字节,中文分隔符应使用 UTF-8 编码后的 []byte,例如 []byte("结束");不要按 rune 下标去计算 advance。
为什么最后一条记录读不到?
通常是 SplitFunc 只查找分隔符,没有在 atEOF 且仍有数据时返回余量。补上 EOF 分支即可读取没有尾部分隔符的记录。
Scanner 报 token too long 怎么办?
如果记录长度有可靠上限,在首次 Scan 前调用 Buffer 设置更大的 max;如果长度不可控或需要分段恢复,应改用 bufio.Reader。
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习