go语言net.conn不处理粘包是tcp字节流特性的正常表现,需应用层通过长度头(4字节大端整数)或分隔符(如\n)协议解决,读取必须用io.readfull确保完整性,并正确处理eof和errunexpectedeof。

Go 语言原生 net.Conn 不处理粘包,直接读取会拿到拼接的多个消息或半个消息——这不是 bug,是 TCP 协议层的正常行为。必须在应用层加协议约定,否则无法可靠解析消息边界。
为什么 conn.Read() 会读到粘包或半包?
TCP 是字节流协议,不保留应用层 send() 的边界。conn.Read() 只按当前内核缓冲区可用数据返回,可能一次读到多个 Write() 的内容,也可能只读到某个 Write() 的前半截。
- 客户端连续调用两次
conn.Write([]byte("hello"))和conn.Write([]byte("world")),服务端一次conn.Read(buf)可能拿到"helloworld" - 客户端发一个 2KB 消息,而服务端
buf只有 1KB,第一次Read()返回 1024 字节,第二次才拿到剩下部分 - 没有长度头、分隔符或固定格式,程序根本不知道“一条完整消息”该截到哪
用消息头 + 长度字段做定长协议(最常用)
每个消息开头写 4 字节大端整数表示后续 payload 长度,接收方先读够 4 字节,再按该长度读取完整 payload。这是 Go 服务间通信最稳妥的做法。
-
binary.Write()写长度头时注意用binary.BigEndian,避免大小端不一致 - 读长度头必须用
io.ReadFull(conn, headerBuf),不能用conn.Read(),否则可能只读到 2 字节就返回 - 读 payload 同样要用
io.ReadFull(conn, payloadBuf),确保读满指定长度 - 示例片段:
headerBuf := make([]byte, 4) if _, err := io.ReadFull(conn, headerBuf); err != nil { return err } msgLen := int(binary.BigEndian.Uint32(headerBuf)) payloadBuf := make([]byte, msgLen) if _, err := io.ReadFull(conn, payloadBuf); err != nil { return err }
用特殊分隔符(如 \n)适合文本协议但要防污染
HTTP、Redis RESP 等文本协议常用换行符做消息边界,Go 可用 bufio.Scanner 配合 SplitFunc 实现,但前提是业务数据本身不能含该分隔符。
- 若原始消息可能含
\n(比如日志内容),必须先做转义(如换为\r\n)或改用其他不可见字符(如0x00),且双方严格约定 -
bufio.Scanner默认最大 token 64KB,超长消息会报scanner: token too long,需提前调scanner.Buffer(nil, 10*1024*1024) - 不要用
strings.Split(string(buf), "\n")手动切分——未读完时buf末尾可能没有\n,导致丢消息
粘包处理最容易被忽略的点
很多人只关注“怎么读”,却忘了“读完后连接状态是否还可用”。io.ReadFull() 返回 io.EOF 时,说明对端已关闭连接,后续所有读操作都会失败;而 io.ErrUnexpectedEOF 表示对方异常断连或发送不完整,此时应主动关闭本端连接,避免 goroutine 泄漏。
另外,如果用循环 for { Read() } 处理连接,务必检查每次 Read() 的返回字节数:0 字节不等于错误,但意味着对端关闭了写入,应退出循环而不是继续读。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











