直接用 encoding/binary 解析裸 TCP 流需先解决粘包和字段边界问题,因 net.Conn 是字节流而非消息流,binary.Read 默认按固定字节数读取易错位导致 io.ErrUnexpectedEOF 或字段异常;正确做法是先读协议头获取 payload 长度,再切片并用 bytes.Buffer 封装后调用 binary.Read,且须显式指定字节序、避免非导出字段、手动处理 TLV 等变长结构,并严格校验缓冲区长度,同时设置读写超时防止永久阻塞。
直接用 encoding/binary 解析裸 tcp 流是最轻量、最可控的方案,但必须先解决粘包和字段边界问题;gob 适合 go-to-go 场景,省去协议定义却牺牲跨语言能力;protobuf + websocket 或 grpc 是工程化首选,但引入额外抽象层和生成代码。
为什么不能直接 binary.Read(conn, …)?
因为 net.Conn 是字节流,不是消息流。TCP 不保证一次 Read() 返回一个完整逻辑包——可能少(半包)、可能多(粘包),而 binary.Read 默认从当前 reader 位置读固定字节数,一旦错位,后续所有字段全乱。
- 典型错误现象:
binary.Read报io.ErrUnexpectedEOF,或读出的uint32值明显异常(比如时间戳变成负数) - 正确做法:先读协议头(如前 4 字节为
payload length),再按长度切片,最后用bytes.Buffer封装该切片调用binary.Read - 必须显式指定字节序:
binary.BigEndian或binary.LittleEndian,和协议文档/设备手册严格对齐 - 结构体字段不能含非导出字段(首字母小写),否则
binary.Read直接跳过
如何安全解析 TLV 或变长字段?
binary.Read 只能处理定长字段。遇到 Tag-Length-Value、UTF-8 字符串、动态数组等,必须手动控制读取偏移和边界检查。
- 先用
buf.Next(1)读Tag,再用binary.Uin16(buf.Next(2))解出Length(注意 Length 字段自身也可能变长,需查协议规则) - 再调
buf.Next(int(length))提取值,**必须提前校验buf.Len() >= int(length)**,否则 panic 或静默错位 - C 风格字符串建议用
bytes.TrimRight(data, "\x00")去末尾零再转string(),避免乱码 - 禁用
unsafe.Pointer强制类型转换——结构体填充、对齐、大小在不同 GOARCH 下不一致,Go 1.21+ 运行时会直接 panic
gob 和 protobuf 该怎么选?
选 gob 当且仅当两端都是 Go 程序、追求开发速度与零序列化开销;选 protobuf 当需要跨语言、强 Schema 约束、或已有 IDL 生态(如 gRPC)。
-
gob优势:无需 IDL,struct 改字段自动兼容(新增字段客户端忽略,旧字段服务端设零值);天然支持time.Time、map、嵌套 struct -
gob劣势:无法被 Python/Java 直接解析;编码后数据不可读,调试困难;长连接中若某次Encode()失败(如网络中断),后续所有Decode()都会卡在流中间,必须重连 -
protobuf要求严格配对:.proto 文件变更后,客户端和服务端必须同步更新生成代码;但可配合 WebSocket 或 gRPC 实现真正的双工流控与重试语义 - 不要为了“高性能”强行上 protobuf——如果只是内部监控指标上报,
gob+ TCP 长连接的实测吞吐和延迟通常优于 protobuf + HTTP/2
最易被忽略的一点:无论用哪种序列化,**超时控制必须落在连接层或读写操作上,而不是靠业务逻辑等待**。TCP 连接卡住时,conn.Read() 或 dec.Decode() 会永久阻塞,除非显式设置 conn.SetReadDeadline() 或用带 timeout 的 context 包裹整个流程。
立即学习“go语言免费学习笔记(深入)”;



















