登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 后停止。
SplitFunc 输入数据、EOF边界、前移字节数、当前词元和扫描错误的结构关系说明图
图1:SplitFunc 返回协议结构说明图,展示输入数据、边界标记和三个返回值的关系;它是静态说明图,不是运行截图。

按逗号和分号返回完整 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 上限
Split注册点、Scanner、Buffer上限、EOF、最后token和错误停止的边界关系说明图
图2:EOF 与 Scanner 配置边界说明图,展示最后 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,按逗号、分号或业务分隔符切词就能保持可预测的边界。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>