必须在应用层定义消息边界,推荐4字节大端序长度前缀协议:发送端先写长度再写payload,接收端用io.ReadFull两次读头和体,并循环解析缓冲区剩余字节,同时设读超时防goroutine泄漏。

不能靠字符串内容判断是否粘包或分包——TCP 传的是字节流,string 只是 Go 对字节的解释视图;真正要处理的,是协议层定义的消息边界。用 json.Unmarshal 或 strings.Split 直接解析原始读取的 []byte,十有八九会 panic 或解错。
为什么 string 类型无助于粘包检测
Go 中的 string 是只读字节切片的包装,它不携带长度头、不标记起始位置、也不记录是否完整。你拿到一个 string,只代表「当前读到的这部分字节被解释成 UTF-8 文本」,但完全无法回答:这是一整条消息?还是半条?后面还跟着几条?
- 把
conn.Read(buf)得到的[]byte强转成string后调json.Unmarshal,遇到半截 JSON 就报invalid character或unexpected EOF - 用
strings.Contains(s, "\n")切分,只要用户发一句{"msg":"hello\nworld"},就提前断开,消息错乱 -
string本身没有「协议语义」,它只是展示层;粘包是传输层 + 应用协议共同作用的结果,必须在字节层面做长度校验和缓冲管理
如何用 bytes.Buffer 正确累积并切分字符串类消息
即使你的 payload 是纯文本(比如 JSON、日志行),也不能跳过缓冲区管理。关键不是“是不是字符串”,而是“有没有完整消息体”。
- 每个连接独享一个
*bytes.Buffer,别用全局或复用实例,否则并发写入 panic - 每次
conn.Read(p)后,立刻buf.Write(p)进入缓冲区 - 进入
for buf.Len() >= 4循环:先读 4 字节头 → 解出n := binary.BigEndian.Uint32(header)→ 若buf.Len() ,跳出等下次数据;否则 <code>buf.Next(4)跳过头,再buf.Next(int(n))取 body - 取出的
[]byte才转string做后续处理(如json.Unmarshal),此时它才是完整、可信的
哪些场景下真能用分隔符,但必须加防护
仅当你能 100% 控制 payload 内容且无嵌套结构时,\n 或 [SEP] 才可考虑。但生产环境几乎不存在这种理想情况,所以必须补三道防线:
立即学习“go语言免费学习笔记(深入)”;
- 发送端对所有可能含分隔符的字段做转义,例如把
\n替换为\r\n,并在接收端反向还原 - 接收端不用
bufio.Scanner默认的ScanLines,而用自定义SplitFunc,确保只匹配「结尾的未转义分隔符」 - 每条消息加 CRC 或简单校验和字段,防止转义失败导致错切;一旦校验失败,直接丢弃该段并重同步(比如找下一个合法分隔符)
- 绝对不要在二进制协议里混用分隔符——
0x0A是合法 payload,strings.Index会误判
最容易被忽略的缓冲区残留问题
一次 conn.Read 返回 1200 字节,可能包含:1 条 500 字节消息 + 1 条 600 字节消息 + 剩余 100 字节(下一条消息的开头)。如果解析完前两条就清空 buf,那 100 字节就丢了——下次读到新数据时,它会跟在新数据前面,变成“垃圾头”。
- 必须用
buf.Next(n)消费已处理部分,而不是buf.Reset() - 若用切片游标(
data := buf.Bytes()+start索引),记得每次处理后更新start += msgLen + 4,且buf = buf[start:]要用copy或buf.Truncate显式收缩,避免内存泄漏 - 长度校验必须做上限检查,比如
if msgLen > 1024*1024 { conn.Close(); return },否则恶意构造超长头会导致 OOM


















