conn.Read 会返回半包或粘包数据,因为 TCP 是字节流协议,不感知消息边界;必须用 io.ReadFull 读固定长度头,配合 bytes.Buffer 缓冲累积解包,gob.Decoder 虽隐式缓冲但不可混用原始读取。

为什么 conn.Read 会返回半包或粘包数据
因为 net.Conn 是裸 TCP 字节流接口,Read() 只返回内核缓冲区当前就绪的字节,不感知业务消息边界。一次调用可能只读到 4 字节头中的 2 字节(半包),也可能读到两个完整消息拼在一起(粘包),甚至一个消息被拆成三次 Read() 返回。这不是 Go 的 bug,而是 TCP 协议本质——它只保证有序、可靠传输字节,不负责“消息”封装。
必须用 io.ReadFull 读长度头,不能靠循环 Read 补齐
io.ReadFull() 是解析固定长度头部的刚性要求。比如用 4 字节 uint32 表示 body 长度,就必须一次性读满这 4 字节。若用原始 conn.Read() 自己循环补读,极易出错:
- 没处理
io.ErrUnexpectedEOF导致 panic - 忘记检查返回字节数,把部分头当完整头解析,
binary.BigEndian.Uint32()得到错误值(如0xffffffff) - 后续按错误长度分配内存,触发 OOM 或负长度 panic
正确写法:var header [4]byte; _, err := io.ReadFull(conn, header[:]) —— 它要么填满,要么明确报错。
bytes.Buffer 是最实用的粘包+半包缓冲方案
真实网络中,一次 conn.Read() 可能含 0 个完整包 + 半包、1 个完整包 + 半包、或多个完整包 + 半包。必须用缓冲区持续累积、滚动消费:
立即学习“go语言免费学习笔记(深入)”;
- 每个连接独享一个
*bytes.Buffer,禁止全局复用或跨 goroutine 共享 - 读 goroutine 每次
conn.Read(buf)后立即buffer.Write(buf) - 解包逻辑循环检查:
buffer.Len() >= 4→ 提取头 →buffer.Len() >= 4 + msgLen→buffer.Next(4 + msgLen)提取完整包 - 不满足条件时直接 break,等下次数据到达;绝不丢弃、不阻塞、不假设
别在 buffer 上调 Read() 取头——它会移动内部 offset,破坏后续判断。
gob.Codec 不是自动解粘包,而是靠 Decoder 原子性隐式缓冲
gob.Decoder.Decode() 看似“自动”处理粘包,实际依赖其原子读取行为:每次调用都阻塞等待一个完整 gob 值(含 magic header、type table、value),多余字节留在内部 bufio.Reader 缓冲区供下次复用。但这有硬约束:
- 不能在同一个
conn上混用gob.Decoder和原始conn.Read()—— 缓冲区状态立刻错乱 - 每个连接必须独占一个
gob.Decoder实例,禁止多个 goroutine 共用 - 无法插入心跳帧或控制指令,gob 没扩展字段机制
- 若需自定义协议(如加长度头),必须绕过
gob.Codec,自己实现ServerCodec
真正健壮的实现,要把解包逻辑和连接生命周期绑定:用 context.WithTimeout() 包裹所有 I/O,设 conn.SetReadDeadline(),并在每次 io.ReadFull() 前检查 ctx.Err(),而不是等超时被动回收。


















