Go处理TCP粘包半包问题必须在应用层定义消息边界,核心是用4字节大端序长度头协议:先io.ReadFull读头解出长度,再io.ReadFull读体,循环解析缓冲区剩余字节,并设读超时防goroutine泄漏。

Go 语言网络通信中出现粘包或半包,不是代码写错了,而是你没在应用层定义消息边界——net.Conn.Read 从不承诺一次只返回一个业务消息,它只按内核缓冲区当前有多少字节就返回多少。排查必须围绕“边界是否被正确识别、剩余字节是否被保留、超时是否生效”这三点展开。
为什么 io.ReadFull 必须用两次,且不能替换成 Read
常见错误是用 conn.Read(header) 读长度头,再用 conn.Read(payload) 读体。这会导致半包卡死:如果 header 只收到前 2 字节,Read 返回 2 并不报错;如果 payload 实际要读 1024 字节但只到了 512,Read 同样返回 512,后续解析直接 panic。
-
io.ReadFull(conn, header)会阻塞直到填满header(比如 4 字节),否则返回io.ErrUnexpectedEOF -
io.ReadFull(conn, payload)同理,确保 payload 长度严格等于 header 解析出的值 - 漏掉任一
ReadFull,就等于放弃对“完整头/体”的校验,半包问题立刻暴露
如何验证解包逻辑是否处理了“剩余字节”
一次 ReadFull 调用可能读到:1 个完整包 + 半个包,或 3 个完整包 + 半个包。如果解包函数返回后直接丢弃缓冲区,粘包里的后半截就永远丢失。
- 解包函数签名应为
func decode(buf []byte) (packet []byte, rest []byte, err error),明确分离已消费和未消费部分 - 别用
bytes.Buffer每次Reset()——它会 realloc,且无法 cheaply slice;推荐用切片游标:buf = buf[n:],原地移动 - 测试时故意发两个短包(如 5B+3B)拼成 8B 一次送达,看服务端是否能拆出两个独立包,而非只处理第一个
为什么 conn.SetReadDeadline 不是可选项而是必选项
没有读超时,一个坏连接就能让 goroutine 永久阻塞——比如客户端只发了 3 字节 header 就断开,io.ReadFull 会一直等剩下 1 字节,goroutine 泄漏。
立即学习“go语言免费学习笔记(深入)”;
- 必须在每次
ReadFull前设置,例如conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 超时时间要略大于最大单包传输耗时(考虑网络抖动),但不能设成 0 或过长(如 1 小时)
- 错误检查不能只判
err != nil,要区分net.ErrTimeout和真实业务错误,前者应 close conn,后者可重试
最容易被忽略的三个点
上线前压测看不出问题,但长连接跑几天后开始丢数据、结构体字段全零、json.Unmarshal 报 invalid character,大概率就是这三个地方没守住:
- 长度字段用了
int而非uint32:32 位系统下int是 4 字节,64 位下是 8 字节,跨平台必错 - 没校验长度上限:收到
length=2GB就 malloc,OOM 或被 DoS - 发送端写了 header 就中断(比如 panic 或 conn.Close()),接收端卡在
ReadFull等 body,超时前无任何反馈


















