Go Scanner 连续调用 Scan 后 Bytes 为什么会变化
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 指向的字节复制一份。因输入长度、缓冲区移动和分词方式不同,旧内容可能全部变化,也可能只有部分变化,因此不要依赖某一次偶然“看起来正常”的结果。

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 之前。

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 检查,并用内容独立性测试验收即可。
MySQL JSON_TABLE 的 ON EMPTY 和 ON ERROR 怎么分别处理
- 上一篇
- MySQL JSON_TABLE 的 ON EMPTY 和 ON ERROR 怎么分别处理
- 下一篇
- Go //go:fix inline 怎么处理带副作用的实参
-
- Golang · Go问答 | 37分钟前 |
- Go bufio.Scanner 遇到 ErrFinalToken 什么时候停止
- 116浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · 可观测性 · Go OpenTelemetry trace 服务依赖
- Go OpenTelemetry 怎么从 Trace 看清服务依赖关系
- 335浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go StdoutPipe 为什么必须在 Wait 前读完
- 497浏览 收藏
-
- Golang · Go问答 | 4小时前 | 错误处理 · Go问答 · 文件系统 · Go path/filepath 符号链接 EvalSymlinks ErrNotExist
- Go EvalSymlinks 为什么在目标不存在时失败
- 223浏览 收藏
-
- Golang · Go问答 | 4小时前 |
- Go os.ReadDir 为什么目录项默认按文件名排序
- 307浏览 收藏
-
- Golang · Go问答 | 5小时前 |
- Go context 的键为什么不该直接使用字符串
- 472浏览 收藏
-
- Golang · Go问答 | 5小时前 |
- Go context.AfterFunc 为什么停止后回调仍可能运行
- 259浏览 收藏
-
- Golang · Go问答 | 9小时前 |
- Go HTTP Trailer 为什么必须先声明再写值
- 431浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 343次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 400次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 395次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 357次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 182次使用
-
- Go 配置参数越来越多怎么办:Functional Options 模式的适用边界和反例
- 2026-07-19 100浏览
-
- Go bufio.Scanner 读取超长行怎么安全扩容
- 2026-09-08 100浏览
-
- Go deferclose 怎么处理关闭返回值
- 2026-09-13 100浏览
-
- Go fmt.Fscanln 与 Fscan 对换行处理有什么区别
- 2026-10-04 100浏览
-
- Go map 并发读写为什么会 panic:从 fatal error 到三种修法
- 2026-07-07 102浏览

