当前位置:首页 > 文章列表 > Golang > Go问答 > Go Scanner 连续调用 Scan 后 Bytes 为什么会变化

Go Scanner 连续调用 Scan 后 Bytes 为什么会变化

来源:17golang原创 2026-10-05 16:40:57 0浏览 收藏

Scanner.Bytes() 返回的是最近一次 Scan 产生的 token,但这段切片可能直接指向 Scanner 正在复用的底层缓冲区。下一次 Scan 为新 token 读取或整理数据时,同一片底层数组可能被覆盖,所以之前保存的 []byte 内容也会随之变化。

只在当前循环内读取或解析 token,可以直接用 Bytes();只要数据要跨越下一次 Scan 保存,就先复制,或者改用返回独立字符串的 Text()。

先做一个会暴露问题的小工具

目标很小:把多行输入扫描成 token,并在扫描结束后统一打印。下面这段“看起来合理”的代码把每次得到的切片直接追加到结果中:

package main

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

func main() {
    input := "red\ngreen\nblue\n"
    scanner := bufio.NewScanner(strings.NewReader(input))

    var tokens [][]byte
    for scanner.Scan() {
        // 错误点:这里只保存切片描述,不会复制 token 的底层字节。
        tokens = append(tokens, scanner.Bytes())
    }
    if err := scanner.Err(); err != nil {
        // 扫描结束后必须检查非 EOF 错误。
        panic(err)
    }

    for i, token := range tokens {
        // 此处打印发生在所有 Scan 调用之后,旧切片可能已经被覆盖。
        fmt.Printf("%d: %q\n", i, token)
    }
}

问题不在 append(tokens, ...) 本身。外层切片确实增长了,但追加进去的每一项仍然只是一个切片头,里面记录指针、长度和容量;它没有自动把 token 指向的字节复制一份。因输入长度、缓冲区移动和分词方式不同,旧内容可能全部变化,也可能只有部分变化,因此不要依赖某一次偶然“看起来正常”的结果。

Scanner 当前 token 与 Bytes 切片共享底层缓冲区的结构说明图
图1:Scanner、当前 token 与多个 Bytes 切片之间的静态共享关系说明图;切片头彼此独立,但底层字节仍可能属于同一缓冲区。

Bytes 返回的是视图,不是长期快照

Go 标准库文档对这个边界写得很直接:Bytes 不分配新内存,返回切片的底层数组可能在后续 Scan 调用中被覆盖。这里要区分三个对象:

  • Scanner:持有读取状态、分词函数和可复用缓冲区。
  • 当前 token:最近一次 Scan 找到的字节范围。
  • Bytes 返回值:描述这个范围的 []byte 切片,它不承诺拥有独立数组。

Scan 前进到下一个 token 后,Scanner 可以继续利用已有空间。旧切片仍然指向那块空间,因此它观察到的是“这块内存现在的内容”,而不是“调用 Bytes 那一刻的不可变快照”。这和 bufio.Reader.ReadSlice 一类借用缓冲区的 API 是相似的生命周期约束。

需要保存时,显式复制 token

Go 1.20 及以上可以用 bytes.Clone 清楚表达“我要拥有一份独立副本”:

package main

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

func main() {
    scanner := bufio.NewScanner(strings.NewReader("red\ngreen\nblue\n"))
    tokens := make([][]byte, 0, 3)

    for scanner.Scan() {
        // Clone 分配独立数组,后续 Scan 不会改写已保存的 token。
        token := bytes.Clone(scanner.Bytes())
        tokens = append(tokens, token)
    }
    if err := scanner.Err(); err != nil {
        // Err 会报告扫描期间遇到的第一个非 EOF 错误。
        panic(err)
    }

    for i, token := range tokens {
        // 预期依次得到 red、green、blue,且每项内容保持独立。
        fmt.Printf("%d: %q\n", i, token)
    }
}

如果项目需要兼容更早的 Go 版本,可以使用 append 完成同样的复制:

for scanner.Scan() {
    // 从 nil 切片开始追加,会为当前 token 创建独立存储。
    token := append([]byte(nil), scanner.Bytes()...)
    tokens = append(tokens, token)
}

另一种常见写法是 copy:

src := scanner.Bytes()
dst := make([]byte, len(src))
// copy 把当前 token 的字节写入新分配的目标切片。
copy(dst, src)
tokens = append(tokens, dst)

三种写法的关键都不是语法,而是让保存结果拥有独立底层数组。复制必须发生在下一次 Scan 之前。

Scanner Bytes 借用切片与独立副本的所有权边界结构图
图2:借用区与持久化区的静态边界说明图;bytes.Clone、append 复制或 copy 会把 token 放入独立存储。

Bytes、Text 和立即消费怎么选

做法是否产生独立数据适合场景注意点
scanner.Bytes()否当前循环内立即解析、比较、写出不要跨越下一次 Scan 保存
bytes.Clone(scanner.Bytes())是需要保留二进制 token每个保存项都会分配并复制
scanner.Text()是,返回新分配字符串后续按文本处理或作为 map 键结果类型是 string

如果只是把 token 解析成整数、立即写入哈希、同步发送给不保留切片的函数,直接使用 Bytes 可以避免不必要的复制。反过来,只要接收方会异步处理、放进队列、缓存到结构体或扫描结束后再使用,就应该把所有权边界写清楚并复制。

还要注意:把借用切片传给 goroutine 尤其危险。即使 goroutine 很快启动,也无法保证它在下一次 Scan 前读完。应当在启动 goroutine 之前完成复制:

for scanner.Scan() {
    // 先取得独立副本,再交给并发任务,避免与 Scanner 复用缓冲区产生数据竞争。
    token := bytes.Clone(scanner.Bytes())
    go func(data []byte) {
        // 并发任务只读取自己拥有的副本。
        process(data)
    }(token)
}

给小工具加上独立性验收

不需要比较内存地址才能判断是否修好。更稳定的验收方式是:保存第一个 token 后继续扫描,再确认保存值仍然等于原内容。下面是可放进项目的测试:

package main

import (
    "bufio"
    "bytes"
    "strings"
    "testing"
)

func TestSavedTokenIsIndependent(t *testing.T) {
    scanner := bufio.NewScanner(strings.NewReader("alpha\nbeta\n"))

    if !scanner.Scan() {
        // 第一次扫描必须成功,否则测试没有可保存的 token。
        t.Fatal("missing first token")
    }
    saved := bytes.Clone(scanner.Bytes())

    if !scanner.Scan() {
        // 继续扫描会推进 Scanner,但不应影响 saved。
        t.Fatal("missing second token")
    }
    if err := scanner.Err(); err != nil {
        t.Fatalf("scan failed: %v", err)
    }
    if string(saved) != "alpha" {
        t.Fatalf("saved token changed: %q", saved)
    }
}

这个测试验证的是业务真正关心的性质:保存值在 Scanner 前进后仍保持独立。它不会绑定 Scanner 当前实现的具体缓冲布局,因此比依赖容量、地址或某组输入的偶然表现更可靠。

常见疑问

把 Bytes 追加到 [][]byte 为什么不算复制?

因为追加的是切片描述,底层数组仍可共享。只有复制字节本身,才会形成独立数据。

每次都用 Text 就一定更好吗?

不是。只在当前循环内消费字节时,Bytes 可以少一次分配;需要长期保存文本时,Text 更直观。选择取决于生命周期和目标类型。

调用 Scanner.Buffer 能阻止旧 Bytes 变化吗?

不能。Buffer 控制初始缓冲区和最大 token 大小,不会把 Bytes 改成拥有独立数组的快照。

什么时候必须检查 Scanner.Err?

扫描循环结束后都应检查。Scan 返回 false 既可能是正常 EOF,也可能是 I/O 错误或 token 过大;Err 用来区分这些情况。

结论

Scanner.Bytes 的变化是有意的零额外分配设计,不是随机故障。把它当作“只在下一次 Scan 前有效的借用视图”就容易判断:立即消费时直接用;跨循环、跨 goroutine 或长期保存时先复制;文本场景也可直接用 Text。最后保留 Err 检查,并用内容独立性测试验收即可。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
MySQL JSON_TABLE 的 ON EMPTY 和 ON ERROR 怎么分别处理MySQL JSON_TABLE 的 ON EMPTY 和 ON ERROR 怎么分别处理
上一篇
MySQL JSON_TABLE 的 ON EMPTY 和 ON ERROR 怎么分别处理
Go //go:fix inline 怎么处理带副作用的实参
下一篇
Go //go:fix inline 怎么处理带副作用的实参
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    343次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    400次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    395次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    357次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    182次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码