Go bufio.ReadBytes 遇到读超时后怎样继续拼接一条消息
用 bufio.Reader.ReadBytes 读取一条以换行结束的长连接消息时,读期限可能先到,但消息的前半段已经读到了。这时不要因为 err != nil 就丢掉返回的字节:官方语义是,找不到分隔符时,ReadBytes 会把错误发生前读到的数据和错误一起返回。正确做法是先把 part 累加到消息缓冲区,确认错误属于可重试的超时后,给同一个连接设置新的读取期限,再继续调用同一个 bufio.Reader。
核心判断只有两层:返回数据始终先保存;只有net.Error.Timeout()为true的错误才进入有限次续读。读到分隔符代表消息完成,EOF 或其他错误代表这条消息不能继续按原样完成。
官方文档:https://pkg.go.dev/bufio#Reader.ReadBytes
先看清 ReadBytes 的两个返回值
ReadBytes(delim) 会读取到第一个分隔字节,并把分隔字节也放进返回切片。只有返回数据以分隔字节结尾时,错误才是 nil;如果在分隔字节出现前遇到 EOF、网络超时或其他底层错误,返回切片仍可能非空。
以换行消息为例,底层连接已经交付 order=42,但还没有交付最后的换行符。第一次读取可能得到:
data = []byte("order=42")
err = 一个 Timeout() 为 true 的 net.Error
这不是“没有读到内容”,而是“当前调用拿到了未完成的前缀”。bufio.Reader 在内部先保存了这部分字节,再把底层错误交给上层;调用方如果直接返回,就会丢失一条尚未完成的消息。

三种处理方式里,只有一种适合续读
在带读取期限的长连接中,常见做法有三种。第一种遇到任何错误都丢弃当前切片,代码简单,但会截断消息。第二种重新创建 bufio.Reader,看似清理了错误状态,却可能把旧 Reader 中还没消费的缓冲数据一起丢掉。第三种保留当前 Reader,把已经返回的片段写入独立的累加缓冲区,然后只对可恢复超时继续读取。
第三种方式的关键不是“忽略错误”,而是把“数据是否完整”和“本次读取是否暂时结束”拆开判断:
- 切片最后是
\n且错误为nil:消息完整,可以交付。 - 切片非空且错误是超时:先保存切片,再调整 deadline,回到同一个 Reader 继续读。
- 错误是
io.EOF或其他非超时错误:保存已读前缀,但不要把它伪装成完整消息。
用同一个 Reader 把超时片段拼回来
下面的函数以换行符作为消息边界。它同时设置消息字节上限和超时次数上限,避免对一条永远不发结束符的慢消息无限等待或无限占用内存。
package framed
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"time"
)
// readMessage 保留超时前拿到的片段,并在同一连接上续读换行消息。
func readMessage(conn net.Conn, maxBytes, maxTimeouts int, wait time.Duration) ([]byte, error) {
reader := bufio.NewReader(conn)
var assembled bytes.Buffer
timeoutCount := 0
for {
// ReadBytes 即使返回错误,也可能带回分隔符前的有效数据。
part, err := reader.ReadBytes('\n')
if len(part) > 0 {
if assembled.Len()+len(part) > maxBytes {
// 消息超过上限时立即停止,避免慢连接造成无界增长。
return append([]byte(nil), assembled.Bytes()...), fmt.Errorf("message exceeds %d bytes", maxBytes)
}
// bytes.Buffer 保存独立副本,下一次读取不会覆盖已拼接内容。
_, _ = assembled.Write(part)
}
if err == nil {
// ReadBytes 在无错误返回时已经找到分隔符,消息可以交付。
return assembled.Bytes(), nil
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
timeoutCount++
if timeoutCount > maxTimeouts {
// 连续超时超过预算时,把已读前缀和真实错误一起交给调用方。
return append([]byte(nil), assembled.Bytes()...), err
}
// deadline 是绝对时间;续读前必须设置一个新的未来时间点。
if err := conn.SetReadDeadline(time.Now().Add(wait)); err != nil {
return append([]byte(nil), assembled.Bytes()...), err
}
// 不重建 reader,继续消费同一个缓冲读取器的状态。
continue
}
if errors.Is(err, io.EOF) {
// EOF 前没有分隔符,只能返回不完整前缀和 UnexpectedEOF。
return append([]byte(nil), assembled.Bytes()...), io.ErrUnexpectedEOF
}
// 非超时错误不具备本例中的续读条件,原样保留给上层处理。
return append([]byte(nil), assembled.Bytes()...), err
}
}
这里的 part 先于错误分支写入 assembled,这是整个续读逻辑最重要的顺序。超时分支只改变下一次读取的时间预算,不改变消息已经累积的内容,也不重新创建 Reader。
为什么 deadline 更新后可以继续使用同一个 Reader
bufio.Reader 负责保存从底层 Reader 取回的字节,连接的 deadline 则由 net.Conn 控制后续 I/O 等待时间。二者是不同层次的状态:超时错误被本次 ReadBytes 返回后,下一轮可以重新调用底层连接;只要没有业务上主动丢弃 Reader,就不必重建缓冲层。
Go 的 bufio 源码会先把底层 Read 返回的字节计入缓冲区,再记录错误。随后 ReadSlice 会把当前缓冲中的片段和错误返回给 ReadBytes。这正是“超时和部分数据同时出现”的原因,也是为什么不能只看错误而忽略切片。
需要注意,SetReadDeadline 设置的是一个绝对时间点,不是“从每次读取开始计时的固定秒数”。所以每次决定续读,都要设置一个新的未来 deadline;如果 deadline 已经过去,下一次调用仍会立即超时。
超时、EOF 和消息上限要分开
| 返回情况 | 是否保留 data | 是否续读 | 应用层结论 |
|---|---|---|---|
| err 为 nil,data 以分隔符结尾 | 是 | 否 | 交付完整消息 |
| net.Error 且 Timeout() 为 true | 是 | 有限次 | 延长 deadline 后继续 |
| io.EOF,data 没有分隔符 | 是 | 否 | 连接结束,消息不完整 |
| 其他错误 | 是 | 否 | 按真实错误处理 |
| 累计长度超过上限 | 可保留诊断前缀 | 否 | 拒绝无界消息 |
不要用“只要有 data 就重试”代替错误分类。磁盘读取、协议解析或连接关闭错误可能永远不会因为再次调用而恢复;把它们统一当成超时,会让调用方在错误连接上循环。

接入长连接时的落地清单
- 先决定消息分隔符,并明确分隔符是否属于交付内容;
ReadBytes会把它保留在返回切片中。 - 为一条消息设置最大字节数,不要让对端通过不发送分隔符来持续扩大缓冲区。
- 对超时设置最大续读次数或总等待预算,续读前重新计算 deadline。
- 保存每次返回的 data,再判断 error;不能因为错误非空就丢弃当前片段。
- EOF 和非超时错误直接结束当前消息;如果需要重连,应由更上层的连接生命周期逻辑决定。
- 记录累计字节数、超时次数和最终错误类型,这些信息比单独记录“读取失败”更容易定位慢消息。
几个容易混淆的问题
ReadBytes 返回超时后,下一次调用会从头读吗?
不会从应用层的消息头重新开始。已经返回给调用方的片段不会自动留在 ReadBytes 的返回值里,所以需要由调用方累加;同一个 Reader 未消费的底层状态仍应继续使用。
把读取期限设长一点能不能替代拼接?
不能。更长的 deadline 只能减少超时发生概率,不能改变 ReadBytes 在消息未完成时返回“部分数据加错误”的契约。网络抖动、慢发送和服务端停顿仍可能触发超时。
超时后能不能重新创建 bufio.Reader?
不建议。重建 Reader 会让旧缓冲层的未消费数据失去引用;如果确实要更换读取器,应先明确旧 Reader 已经没有要交付的字节,并由协议层重新定义消息边界。
没有固定分隔符时还适合用 ReadBytes 吗?
不适合。此时应改用长度前缀、固定长度或其他明确的帧协议;仅靠不断追加字节无法判断一条消息何时完成,也无法可靠设置消息上限。
参考源码:https://go.dev/src/bufio/bufio.go;网络错误接口:https://pkg.go.dev/net#Error;读取期限:https://pkg.go.dev/net#Conn.SetReadDeadline。
Go gif.GIF 的 LoopCount 为负数时为什么只播放一遍
- 上一篇
- Go gif.GIF 的 LoopCount 为负数时为什么只播放一遍
- 下一篇
- Go io.ReadFull 怎样处理固定长度协议的半包和 EOF
-
- Golang · Go问答 | 29分钟前 |
- Go color.NRGBAModel 转换颜色时怎样避免 alpha 预乘
- 183浏览 收藏
-
- Golang · Go问答 | 30分钟前 | go · Go 图像合成 image/draw DrawMask 透明遮罩
- Go draw.DrawMask 怎样用透明遮罩合成局部图像
- 464浏览 收藏
-
- Golang · Go问答 | 33分钟前 |
- Go ast.Preorder 怎样用 range 遍历语法树节点
- 278浏览 收藏
-
- Golang · Go问答 | 34分钟前 |
- Go image.NRGBA 写入半透明像素后颜色为什么变了
- 135浏览 收藏
-
- Golang · Go问答 | 36分钟前 | Go go/scanner 词法错误 token.File token.Position scanner.Scanner
- Go scanner.Scanner 怎样让词法错误带上源文件名
- 126浏览 收藏
-
- Golang · Go问答 | 40分钟前 | go · 工具链 · Go 版本字符串 IsValid go/version 工具链版本
- Go go/version.IsValid 为什么会拒绝看似正确的版本字符串
- 175浏览 收藏
-
- Golang · Go问答 | 43分钟前 |
- Go go/types.Alias 怎样读取泛型别名的 TypeParams 和 TypeArgs
- 233浏览 收藏
-
- Golang · Go问答 | 43分钟前 |
- Go math/bits.Mul64 返回的 hi 和 lo 怎么还原完整乘积
- 372浏览 收藏
-
- Golang · Go问答 | 43分钟前 | go · 工具链 · Go Beta Compare go/version rc
- Go go/version.Compare 怎样比较带 rc 和 beta 的版本字符串
- 278浏览 收藏
-
- Golang · Go问答 | 46分钟前 |
- Go http.ParseSetCookie 解析失败时怎样定位非法属性
- 433浏览 收藏
-
- Golang · Go问答 | 50分钟前 |
- Go crypto/mlkem 公钥和私钥怎样按字节格式保存与恢复
- 255浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 891次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 891次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 976次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 961次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 788次使用
-
- 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浏览

