Go语言不处理TCP粘包拆包,因net.Conn是无消息边界的字节流;粘包拆包由TCP流特性、缓冲区、MTU、Nagle算法等导致;需用长度前缀、分隔符等方案手动解包,bufio.Reader仅缓存不解决边界问题。

Go 语言本身不自动处理 TCP 粘包与拆包,net.Conn 提供的是字节流接口,没有消息边界 —— 这意味着你读到的 []byte 可能包含多个业务消息、半个消息,或刚好一个完整消息,全看网络传输和缓冲时机。
为什么 Go 的 conn.Read() 会粘包或拆包
TCP 是面向字节流的协议,OS 内核和 Go runtime 的 socket 缓冲区、MTU 分片、Nagle 算法、应用层调用时机共同导致:一次 conn.Read() 返回的数据量不可预测。它只保证「最多读取 len(buf) 字节」,不保证「读完一个逻辑包」。
- 客户端连续两次
conn.Write([]byte{1,2})和conn.Write([]byte{3,4,5}),服务端一次Read()可能拿到[]byte{1,2,3,4,5}(粘包) - 客户端发一个 2KB 的 JSON 消息,但服务端
Read()第一次只返回前 1024 字节(拆包) -
Read()返回n > 0不代表消息结束;返回n == 0也不代表连接关闭(可能是 EAGAIN/EWOULDBLOCK)
常见解包方案及 Go 实现要点
没有银弹,选型取决于协议设计权是否在你手上。优先级从高到低:
-
带长度前缀(推荐):在每条消息前写入 4 字节大端整数表示 body 长度。Go 中用
binary.BigEndian.PutUint32()编码,用io.ReadFull()确保读够长度字段 + 消息体 -
分隔符(如
\n或\x00):适合文本协议(如 Redis RESP)。用bufio.Scanner配合自定义SplitFunc,但注意:分隔符若出现在 payload 中需转义,否则误切 - 固定长度帧:仅适用于极简场景(如传感器每帧 32 字节),灵活性差,浪费带宽
- 协议内嵌长度字段(如 Protobuf + 自定义 envelope):需先读 header 解出 size,再读 body;比纯长度前缀多一次解析,但利于扩展
示例(长度前缀):
立即学习“go语言免费学习笔记(深入)”;
// 发送端
size := uint32(len(data))
buf := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(buf[:4], size)
copy(buf[4:], data)
conn.Write(buf)
// 接收端(需循环读取直到凑够 size)
var sizeBuf [4]byte
if _, err := io.ReadFull(conn, sizeBuf[:]); err != nil {
return err
}
size := binary.BigEndian.Uint32(sizeBuf[:])
body := make([]byte, size)
if _, err := io.ReadFull(conn, body); err != nil {
return err
}
bufio.Reader 能否直接解决粘包
不能。它只是包装了底层 conn.Read(),提供缓存和按行/按字节读取能力,但依然无法跨调用感知消息边界。比如 reader.ReadString('\n') 在遇到粘包时会正确截断第一个完整行,但剩余数据留在 buffer 中 —— 下次调用才会继续读,这反而是正确行为;问题在于你得确保业务逻辑不假设「每次 ReadString 就是一次完整请求」,而要容错处理 buffer 中残留部分。
- 用
bufio.Scanner时,Scan()失败后,Err()可能是bufio.ErrTooLong(超长行被截断),此时 scanner 内部 buffer 已丢弃部分数据,不可恢复 -
bufio.Reader.Peek()可预读判断是否有完整包头,但 Peek 不移动读位置,需配合Discard()或手动管理 offset,易出错 - 不要混用
conn.Read()和bufio.Reader.Read(),它们共享底层 conn 但各自维护 buffer,会导致数据丢失或错位
生产环境必须检查的三个细节
很多实现在线上跑几天才暴露问题,根源常在这三处:
-
read timeout 设置不合理:使用
conn.SetReadDeadline(),而非全局conn.SetDeadline();超时应覆盖「读长度头」和「读 body」两个阶段,否则卡在半包时连接永远 hang 住 -
未处理
io.EOF与io.ErrUnexpectedEOF区别:io.ReadFull()在连接提前关闭时返回后者,表示预期读满却断连,需主动 close conn 并 log;而io.EOF是正常关闭信号 -
并发读写未加锁或未用 goroutine 隔离:TCP 连接不是线程安全的 —— 同一
conn上多个 goroutine 调用Write()会数据交错;建议每个连接配一个 reader goroutine + 一个 writer goroutine,用 channel 串行化写请求
粘包本质是协议层缺失,Go 不替你做决定。真正健壮的实现,永远从「定义清晰的消息格式」开始,而不是在 Read() 返回后猜边界。


















