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

Go bufio.Scanner 怎样用 SplitFunc 读取自定义记录

来源:17golang原创

时间:2026-11-01 12:25:09 135浏览 收藏

当输入不是“一行一条”,而是由 \n---\n、空字节或其他业务标记分隔的文本记录时,bufio.Scanner 仍然可以使用,但必须先提供一个符合 SplitFunc 契约的切分函数。核心规则只有三点:找到完整记录时返回要跳过的字节数和 token;数据暂时不完整时返回 (0, nil, nil);读到 EOF 后仍有尾部数据时把它作为最后一条记录交给调用方。

下面用 \n---\n 分隔日志块,每个块内部保留原始文本。示例没有依赖本地文件或运行截图,重点是说明输入边界、内存上限和错误出口。

先把自定义记录边界说清楚

假设输入是下面这种形式:

id=1001;level=info;msg=connected
---
id=1002;level=warn;msg=retrying
---
id=1003;level=error;msg=timeout

这里的分隔符是完整的五个字节 \n---\n。分隔符不属于 token,因此 advance 必须越过它;最后一条记录不要求以分隔符结尾,EOF 时也应返回。

Go Scanner 输入缓冲区与 SplitFunc 返回 advance、token、atEOF 的静态关系图
图1:SplitFunc 读取自定义记录时,输入缓冲、EOF 状态和三个返回值之间的静态关系说明图。

SplitFunc 的三个返回值分别负责什么

SplitFunc 的签名是 func(data []byte, atEOF bool) (advance int, token []byte, err error)。data 是尚未处理的输入片段,atEOF 表示底层 Reader 已经没有更多数据。返回值的含义如下:

返回值作用常见写法
advance告诉 Scanner 从当前输入中跳过多少字节分隔符结束位置,即 i + len(delim)
token交给 Scan 后的当前记录分隔符之前的 data[:i]
err报告切分失败或用特殊哨兵结束正常读取返回 nil

如果当前缓冲区还没有完整分隔符,函数不能猜测一条记录已经结束,也不能把输入向前推进;应返回 0, nil, nil,让 Scanner 继续读取更多数据。

用闭包实现按标记分隔的记录读取

下面的函数把分隔符作为闭包变量,既可以复用同一套切分逻辑,也能让分隔符的含义集中在一个位置:

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "strings"
)

// splitRecords 按完整分隔符切分记录;分隔符本身不进入 token。
func splitRecords(delim []byte) bufio.SplitFunc {
    return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        if i := bytes.Index(data, delim); i >= 0 {
            // advance 越过分隔符,避免下一次扫描重复看到它。
            return i + len(delim), data[:i], nil
        }

        if atEOF {
            // EOF 时把没有分隔符收尾的最后一段交付出去。
            if len(data) == 0 {
                return 0, nil, nil
            }
            return len(data), data, nil
        }

        // 分隔符可能被拆在两次读取之间,等待更多字节。
        return 0, nil, nil
    }
}

func main() {
    input := "id=1001;level=info;msg=connected\n---\nid=1002;level=warn;msg=retrying\n---\nid=1003;level=error;msg=timeout"
    scanner := bufio.NewScanner(strings.NewReader(input))

    // 在第一次 Scan 前设置自定义切分函数。
    scanner.Split(splitRecords([]byte("\n---\n")))
    // 限制单条记录的最大缓冲,避免异常长记录无限膨胀。
    scanner.Buffer(make([]byte, 32), 256*1024)

    for scanner.Scan() {
        // Text 会生成独立字符串;处理逻辑不应依赖下一次 Scan 后的 Bytes。
        fmt.Printf("record=%q\n", scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        // Scan 返回 false 既可能是 EOF,也可能是读取或切分错误。
        fmt.Printf("scan failed: %v\n", err)
    }
}

这里最容易写错的是 EOF 分支:atEOF 且 data 非空时,剩余字节就是最后一条记录;若输入刚好以分隔符结尾,剩余数据为空,就不要凭空再生成一条空记录。

记录内容需要校验时,把错误放进 SplitFunc

切分和内容校验可以组合,但要保持职责清楚:分隔符负责确定边界,字段解析负责判断内容是否合法。若希望扫描在遇到坏记录时停止,可以在返回 token 前检查前缀:

func splitCheckedRecords(delim []byte) bufio.SplitFunc {
    return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        i := bytes.Index(data, delim)
        if i 

上面的示例展示的是错误出口,实际项目中可以返回带有记录偏移、字段名或脱敏上下文的错误。不要在错误信息里拼接未经处理的整条外部输入,以免日志膨胀或泄露敏感内容。

Scanner.Buffer 决定单条记录的资源边界

Scanner 默认有 token 大小上限。自定义记录很容易比普通行更长,因此应在开始扫描前调用 Buffer,并根据业务协议设置合理的最大值。Buffer 只能在第一次扫描前调用;如果调用后仍遇到超长 token,Scan 会停止,错误从 Err 取出。

Go Scanner.Buffer、最大 token、Scan、Bytes、Text 与 Err 的边界关系图
图2:Scanner 的 token 大小控制、消费接口、错误出口与 bufio.Reader 替代边界说明图。

长度上限不是“越大越好”。上限太小会拒绝合法记录,上限太大则允许一次读取占用更多内存。若记录可能非常大、需要精细控制读取进度,或者要在一个 Reader 上顺序完成多种读取,官方文档建议考虑 bufio.Reader。

Bytes、Text 和错误检查不要混用错

  • scanner.Text() 返回新分配的字符串,适合把当前记录交给后续逻辑保存。
  • scanner.Bytes() 不额外分配,但底层数组可能在下一次 Scan 后被覆盖;若要异步处理或长期保存,必须复制。
  • 循环结束后一定检查 scanner.Err()。正常 EOF 会被转换为 nil,读取错误、token 过长或 SplitFunc 返回的错误不会静默消失。
for scanner.Scan() {
    token := scanner.Bytes()
    // 只在当前 Scan 生命周期内使用 token,避免引用下一轮会复用的缓冲。
    handleRecord(token)
}

if err := scanner.Err(); err != nil {
    // 将扫描错误交给上层,区分正常 EOF 与异常停止。
    return fmt.Errorf("read records: %w", err)
}

几个容易让 SplitFunc 失效的边界

没有完整分隔符就返回 token。这会把一条记录拆开。输入片段可能只包含分隔符的一半,只有返回 0, nil, nil 才能请求更多数据。

advance 没有越过分隔符。下一次调用仍会从相同位置看到分隔符,轻则产生空记录,重则导致输入无法前进。advance 必须是非负且不超过当前数据长度。

EOF 时无条件返回空 token。如果协议不把末尾空记录作为有效数据,len(data)==0 时应直接返回空结果;若确实需要交付空的最后一条记录,可以参考 bufio.ErrFinalToken 的语义单独设计。

把 Scanner 当成无限大消息解析器。Scanner 的优势是简单的顺序 token 化,而不是处理任意大小消息。超长记录、需要恢复的解析错误或复杂回退策略,更适合用 bufio.Reader 配合明确的读取协议。

速查:什么时候该选哪种写法

场景建议
固定字符串分隔的中小型文本记录Scanner.Split(customSplit),同时设置 Buffer
记录长度有明确上限但可能超过默认值提高 Buffer 的最大值,并处理 Err
记录可能非常大或需要分段恢复使用 bufio.Reader,自行管理缓冲和错误
需要保留最后一个空记录在 SplitFunc 中明确使用 ErrFinalToken 的结束语义

归纳起来,SplitFunc 不是“把字符串按分隔符切开”的普通回调,而是 Scanner 与输入流之间的边界契约:advance 负责消费位置,token 负责交付记录,atEOF 负责处理尾部数据。只要不提前推进、不忽略超长错误,并在循环后检查 Err,自定义记录读取就能保持可控。

相关问题

SplitFunc 为什么要返回 0、nil、nil?因为当前缓冲区还没有完整 token,返回这个组合会让 Scanner 继续读数据,而不是丢弃或提前结束。

最后一条记录没有分隔符怎么办?当 atEOF 为真且 data 非空时,把剩余数据作为 token 返回。

Scanner 的 token 太长应该怎么处理?先用 Buffer 设置合理上限;如果记录规模不适合一次放入 token,改用 bufio.Reader。

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