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

Go bufio.Scanner 如何自定义分隔符读取记录

来源:17golang原创

时间:2026-09-12 10:59:31 338浏览 收藏

如果输入不是按换行分隔,而是用 ||@@ 这类业务标记分隔记录,Go 仍然可以继续使用 bufio.Scanner。做法是给 Scanner 注册一个自定义 SplitFunc:找到完整分隔符就返回一条 token,暂时找不到就等待更多字节,读到 EOF 再把末尾余量交出来。

要点速览
  • Scanner.Split 接收的是函数,不是单个字符;多字节分隔符可用 bytes.Index 查找。
  • 没有完整记录时必须返回 0, nil, nilatEOF 时要处理没有尾部标记的最后一条记录。
  • 默认 token 缓冲上限是 64 KiB,长记录先用 Buffer 调整,仍需要更强控制时考虑 bufio.Reader

自定义 SplitFunc 要同时处理分隔符和 EOF

Scanner 的默认切分规则是按行读取;Split 可以把它替换为业务规则。一个 SplitFunc 每次面对当前字节片段时,需要返回“应跳过多少字节”“本次 token 是什么”和“是否出错”三个结果。分隔符还没收完整时,不能急着把片段当成记录。

Go bufio.Scanner 自定义 SplitFunc 中 Scanner、输入字节、分隔符和 token 的静态关系图
图1:看清输入字节、分隔符和 EOF 标记如何共同约束 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需要更强的错误控制和读取过程控制
Go Scanner Buffer 与 token 长度上限及 bufio.Reader 替代边界关系图
图2:对照 token、Buffer 和 Reader 的边界,判断记录过长时该调整 max 还是更换读取器。

什么时候不该继续扩大 Scanner 的 max

Buffer 解决的是“已知上限内的长 token”,不是无限长度输入的通用方案。如果输入来自不可信网络、单条记录可能持续增长,或者程序需要在超长数据中逐段恢复,继续把 max 调到很大只会把风险推迟。此时用 bufio.Reader.ReadStringReadBytes 或更细粒度的读取逻辑,通常更容易控制内存和错误路径。

落地前可以按这份清单判断:分隔符是否可能跨读取边界;末尾无分隔符是否仍算有效记录;连续分隔符是否允许空记录;记录长度上限是多少;token 是否会交给异步任务;扫描结束后是否检查了 Err()。这些问题都明确后,自定义 Scanner 就会从一个小技巧变成稳定的输入边界。

常见问题

SplitFunc 能按中文字符串分隔吗?

可以。Scanner 面向字节,中文分隔符应使用 UTF-8 编码后的 []byte,例如 []byte("结束");不要按 rune 下标去计算 advance

为什么最后一条记录读不到?

通常是 SplitFunc 只查找分隔符,没有在 atEOF 且仍有数据时返回余量。补上 EOF 分支即可读取没有尾部分隔符的记录。

Scanner 报 token too long 怎么办?

如果记录长度有可靠上限,在首次 Scan 前调用 Buffer 设置更大的 max;如果长度不可控或需要分段恢复,应改用 bufio.Reader

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