Scanner自定义SplitFunc切词
来源:17golang原创
时间:2026-11-15 05:53:05 354浏览 收藏
如果输入不是“按行”或“按空白”切分,而是要同时把逗号、分号当作分隔符,可以给 bufio.Scanner 注册一个自定义 SplitFunc。关键不是在函数里循环取词,而是遵守它的返回协议:找到完整 token 时返回前移字节数和 token;数据还不完整时返回 0, nil, nil;到达 EOF 后仍有尾部数据时,把尾部交付出去。
本文用一个“逗号或分号切词”的小例子说明这个协议,同时把长 token、空 token 和错误处理边界讲清楚。
SplitFunc 的返回值到底怎么配合 Scanner
SplitFunc 的签名是 func(data []byte, atEOF bool) (advance int, token []byte, err error)。data 是当前尚未处理的输入片段,atEOF 表示底层 Reader 已经没有更多数据。三个返回值分别决定 Scanner 前移多少字节、交付什么 token,以及是否停止扫描。
advance必须表示已经消费的字节数,不能为负数。token非空时,下一次Scan可以通过Bytes或Text取到它。- 数据不足以组成一个完整 token 时返回
0, nil, nil,让 Scanner 继续读数据。 err非空会终止扫描;特殊的bufio.ErrFinalToken可用于交付最后 token 后停止。

按逗号和分号返回完整 token
实现时可以先找出当前数据片段中的第一个分隔符。找到后,返回“分隔符之后的位置”作为 advance,而 token 只保留分隔符之前的内容。下面的实现还会去掉 token 两侧空白,但不会把空 token 静默变成一个词。
package main
import (
"bufio"
"bytes"
"fmt"
"strings"
)
// splitFields 把逗号和分号视为分隔符,并保留 Scanner 的增量读取语义。
func splitFields(data []byte, atEOF bool) (advance int, token []byte, err error) {
// 只在当前缓冲区里查找分隔符,不假设一次 Read 就能拿到完整词元。
if i := bytes.IndexAny(data, ",;"); i >= 0 {
// 前移包含分隔符,token 不包含分隔符,避免下一次重复消费它。
return i + 1, bytes.TrimSpace(data[:i]), nil
}
if atEOF {
// EOF 时没有新的数据可读,仍需交付最后一个无分隔符结尾的字段。
if len(bytes.TrimSpace(data)) == 0 {
return 0, nil, nil
}
return len(data), bytes.TrimSpace(data), nil
}
// 当前片段还没有完整分隔符,要求 Scanner 读入更多数据。
return 0, nil, nil
}
func main() {
scanner := bufio.NewScanner(strings.NewReader("Go,Scanner;SplitFunc"))
// Split 必须在第一次 Scan 之前注册。
scanner.Split(splitFields)
for scanner.Scan() {
// Text 会生成独立字符串,适合把当前 token 保存到结果中。
fmt.Println(scanner.Text())
}
// 读取结束后检查底层 Reader 或 SplitFunc 返回的错误。
if err := scanner.Err(); err != nil {
panic(err)
}
}
这个函数的核心是“未找到分隔符时不前移”。如果此时直接返回 len(data),Scanner 可能把一个被分块读入的字段提前截断;如果返回正数却不给 token,也会改变下一轮输入的起点。对于空字段,是否保留要由业务决定:当前示例返回空切片,调用方可以在 Scan 循环中显式过滤。
EOF、空 token 与 Scanner 接入
EOF 不是错误,它只是告诉 SplitFunc 当前不会再有更多字节。最容易遗漏的是最后一个字段:输入以普通字符结束时,分隔符搜索会失败,但 atEOF 为真,此时应该把剩余数据作为最后 token 返回。
注册和容量设置也有明确边界:
| 场景 | 返回或配置 | 作用 |
|---|---|---|
| 数据不完整 | 0, nil, nil | 请求 Scanner 继续读 |
| EOF 仍有尾部字段 | len(data), token, nil | 交付最后 token |
| 需要提前终止 | token, bufio.ErrFinalToken | 无错误结束扫描 |
| 字段可能很长 | scanner.Buffer(buf, max) | 提高允许的 token 上限 |

// 在 Scanner 开始读取前同时设置自定义切词和 token 上限。
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
scanner.Split(splitFields)
for scanner.Scan() {
// 如果业务不接受空字段,可在这里统一过滤。
field := strings.TrimSpace(scanner.Text())
if field == "" {
continue
}
consume(field)
}
// Scan 返回 false 后仍要读取 Err,区分 EOF 与实际错误。
if err := scanner.Err(); err != nil {
return err
}
Split 和 Buffer 都应在扫描开始前调用;一旦第一次 Scan 已经启动,再修改配置会触发 panic。另一个边界是 Scanner 适合有限大小的 token:如果字段可能远超设置的上限,或业务需要更细的读取和恢复策略,应改用 bufio.Reader,不要只把上限无限调大。
常见问题
为什么最后一个词经常丢失?
通常是 SplitFunc 只处理了分隔符命中的分支,没有在 atEOF 为真且 data 非空时返回尾部 token。
为什么 Scanner 读到一半就停止?
先检查 scanner.Err()。可能是 SplitFunc 返回了错误,也可能是 token 超过 Scanner 当前允许的最大尺寸;后者应在扫描前用 Buffer 设置合理上限,或换用 bufio.Reader。
什么时候返回 ErrFinalToken?
当业务已经拿到想要的最后一个 token,不希望 Scanner 继续读取时可以使用它。普通的“读完所有字段”场景不需要主动返回这个特殊错误。
总结:自定义 SplitFunc 的稳定写法是先处理已有分隔符,再处理 EOF 尾部,最后用 0, nil, nil 表示“请继续取数据”。只要始终让 advance 与已消费字节一致,并在 Scan 结束后检查 Err,按逗号、分号或业务分隔符切词就能保持可预测的边界。
-
369 收藏
-
344 收藏
-
464 收藏
-
327 收藏
-
467 收藏
-
224 收藏
-
159 收藏
-
152 收藏
-
214 收藏
-
384 收藏
-
371 收藏
-
274 收藏
-
473 收藏
-
404 收藏
-
355 收藏
-
478 收藏
-
133 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习