Go bufio.Reader.UnreadRune 与 ReadRune 配对的回退边界
bufio.Reader.UnreadRune 不是“把最近读到的字符退回去”的通用按钮。它只接受紧邻的一次 ReadRune,最多回退一个 UTF-8 符文;如果中间调用了其他读取方法,或已经回退过一次,就会返回 bufio.ErrInvalidUnreadRune。
记住一条调用契约:先用 ReadRune 试读,需要把这个符文交给后续解析时立刻调用一次 UnreadRune;不要在两者之间插入 ReadByte、Read、Peek、Discard 或第二次 UnreadRune。
官方文档地址:https://pkg.go.dev/bufio#Reader.ReadRune、https://pkg.go.dev/bufio#Reader.UnreadRune。实现参考:https://go.dev/src/bufio/bufio.go。
ReadRune 记录了什么,UnreadRune 回退什么
ReadRune 从 Reader 的缓冲区读取一个 UTF-8 编码字符,返回字符本身、占用的字节数和错误。ASCII 字符通常占 1 个字节,常见中文字符占 3 个字节;返回的 size 才是后退时需要使用的长度。
在实现里,读取成功后会把本次符文的字节数记到内部状态 lastRuneSize。UnreadRune 不重新解析字节,也不接收参数,而是依据这个状态把读指针向前移动同样的字节数,然后清空回退资格。所以它只能恢复“最近一次 ReadRune”读出的那个符文。

还要注意无效 UTF-8 的边界:文档规定遇到无效编码时会消耗一个字节,并返回 utf8.RuneError 与大小 1。此时如果紧接着调用 UnreadRune,回退的也是这个已经被当作一个符文处理的字节,不是任意长度的错误片段。
为什么会得到 ErrInvalidUnreadRune
最常见的错误不是 UTF-8 本身,而是调用顺序。下面的示例只展示接口契约,代码块中的注释说明每个分支的意图;不要把两次回退写成一个可重复调用的“撤销”操作。
package main
import (
"bufio"
"errors"
"fmt"
"strings"
)
func main() {
reader := bufio.NewReader(strings.NewReader("Go语言"))
_, _, _ = reader.ReadRune() // 中文注释:先试读一个符文,再决定是否交还给解析器。
if err := reader.UnreadRune(); err != nil { // 中文注释:只能紧接 ReadRune 回退一次。
if errors.Is(err, bufio.ErrInvalidUnreadRune) { // 中文注释:把顺序错误与输入错误分开处理。
fmt.Println("UnreadRune 调用顺序不合法")
return
}
fmt.Println("回退失败:", err)
return
}
// 中文注释:再次读取会重新建立新的配对,但不能连续第二次 UnreadRune。
_, _, _ = reader.ReadRune()
}
核心只有两点:让同一个 Reader 连续完成 ReadRune 与 UnreadRune,并在错误处理里识别 ErrInvalidUnreadRune。为了避免把探测逻辑散落到多个层级,下面给出更适合直接改造的解析片段。
func readPrefixOrPutBack(r *bufio.Reader) (bool, error) {
// 试读一个符文,只有不属于目标前缀时才把它交回去。
ch, _, err := r.ReadRune()
if err != nil {
return false, err
}
if ch == '@' {
return true, nil
}
if err := r.UnreadRune(); err != nil { // 中文注释:回退必须紧跟本次 ReadRune。
return false, err
}
return false, nil
}
哪些调用会打断这次配对
UnreadRune 对顺序比 UnreadByte 更严格。源码中的状态字段会在其他读取动作中失效,因此下面几类写法都不能期待成功。
| 写法 | 结果 | 原因 |
|---|---|---|
| Reader 还没有 ReadRune | 返回 ErrInvalidUnreadRune | 没有可回退的符文大小 |
| ReadRune 后调用 ReadByte | 回退资格失效 | 最近一次读取已不是 ReadRune |
| ReadRune 后调用 Peek | 回退资格失效 | Peek 会清除回退状态 |
| 成功 UnreadRune 后再次 UnreadRune | 返回 ErrInvalidUnreadRune | 一次配对只允许一次回退 |
| ReadRune 返回 EOF | 没有可回退符文 | 没有成功读取字符,也没有有效 size |

因此,解析器如果需要先看一个字符再决定由谁处理,最好把“试读—判断—立即回退”封装在一个小函数里。不要把 Reader 交给多个层级后,让上层的日志、探测或预读调用悄悄插入两次操作之间。
把规则落到实际解析边界
一个可靠的调用方通常遵循三条规则:第一,回退对象只使用最近一次 ReadRune 的返回结果;第二,回退后把 Reader 交还给明确的下一层,不在同一层继续猜测;第三,把 ErrInvalidUnreadRune 当作调用契约错误处理,而不是把它重试成无限循环。
如果业务需要回退多个符文,UnreadRune 就不是合适的缓存设计。可以在应用层保存已经读出的字符串或字节片段,或者改用更适合随机访问的数据结构;不要连续调用 UnreadRune,因为第一次成功后内部回退状态已经被清空。
常见问题
UnreadRune 能回退多个中文字符吗?不能。一次成功调用只回退最近一次 ReadRune 读取的一个符文,中文字符占几个字节不改变“一次一个符文”的契约。
ReadRune 之后调用 Buffered 会打断回退吗?不会把它当作新的读取动作;但为了让调用关系清楚,仍建议把 ReadRune 与 UnreadRune 放在同一段小逻辑里,避免中间再加入会改变状态的读取方法。
为什么 UnreadByte 看起来更宽松?官方文档明确区分了两者:UnreadByte 可以回退最近读取操作得到的最后一个字节,而 UnreadRune 要求最近一次方法就是 ReadRune。
总结
ReadRune 成功后记录的是本次 UTF-8 符文的字节长度,UnreadRune 只依据这份短暂状态回退一次。只要把它当成严格的一对接口:紧邻调用、一次回退、错误及时上交,中文字符和无效 UTF-8 都能按明确的字节边界处理;如果需要多次回看,就应把历史数据放到应用层,而不是强行堆叠 UnreadRune。
Go bufio.Reader.Discard 怎样跳过已读字节并处理短读取
- 上一篇
- Go bufio.Reader.Discard 怎样跳过已读字节并处理短读取
- 下一篇
- Go strings.CutPrefix 怎样去掉固定前缀并判断是否匹配
-
- Golang · Go教程 | 7分钟前 |
- Go errors.Join 怎样组合多个错误并保留匹配能力
- 496浏览 收藏
-
- Golang · Go教程 | 8分钟前 |
- Go strings.Cut 怎样拆分一次并区分是否找到
- 474浏览 收藏
-
- Golang · Go教程 | 13分钟前 |
- Go context.AfterFunc 怎样在取消时执行收尾逻辑
- 425浏览 收藏
-
- Golang · Go教程 | 16分钟前 |
- Go time.NewTimer 怎样正确停止和复用定时器
- 269浏览 收藏
-
- Golang · Go教程 | 17分钟前 |
- Go io/fs.ValidPath 的相对路径校验与目录边界
- 488浏览 收藏
-
- Golang · Go教程 | 27分钟前 | 标准库 · go · Go 位运算 字节序 math/bits.ReverseBytes16 uint16
- Go math/bits.ReverseBytes16 怎样交换整数的高低字节
- 306浏览 收藏
-
- Golang · Go教程 | 34分钟前 | go · strconv ·
- Go strconv.ParseFloat 怎样处理精度和非法输入
- 361浏览 收藏
-
- Golang · Go教程 | 35分钟前 | go · unicode ·
- Go strconv.QuoteToGraphic 怎样把图形字符保留在字符串字面量中
- 242浏览 收藏
-
- Golang · Go教程 | 39分钟前 |
- Go utf8.RuneCountInString 怎样统计中文字符串长度
- 115浏览 收藏
-
- Golang · Go教程 | 41分钟前 | go · unicode ·
- Go unicode.ToLower 怎样按 Unicode 规则转换字符大小写
- 218浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 758次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 756次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 838次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 803次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 635次使用
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- 快速掌握Go语言HTTP标准库的实现方法
- 2022-12-30 327浏览
-
- 解析golang 标准库template的代码生成方法
- 2022-12-24 349浏览

