直接用conn.write(struct)失败因write只接受[]byte且go无隐式转换;真正可用的codec必须同时实现序列化与分帧(如长度前缀),并统一处理压缩、错误传播及跨语言兼容性。

为什么直接用 conn.Write(struct) 一定失败
因为 net.Conn 的 Write 方法只接受 []byte,而 Go struct 没有隐式转换能力。编译器会直接报错:cannot use myStruct (type MyStruct) as type []byte。
常见错误包括:
- 用
fmt.Sprintf("%v", s)拼字符串再转[]byte→ 字段顺序、nil值、time.Time格式全不可控 - 用
json.Marshal(s)后直接Write()→ 解决了序列化,但没解决 TCP 粘包,接收方无法判断消息边界 - 把
gob.Encoder直接套在conn上 →gob是流式编码,不自带长度头,多请求会互相污染
真正能上线的 Codec,必须同时处理两件事:序列化 + 分帧(framing),缺一不可。
ReadRequestHeader 和 WriteRequest 怎么配合长度前缀
标准 rpc.ServerCodec 要求你实现 ReadRequestHeader、ReadRequestBody、WriteResponse 等方法。关键在于:长度前缀不能只加在 body 上,而要覆盖整个逻辑消息单元(含 header + body),否则 header 读取逻辑会错位。
推荐做法是把「header」理解为 RPC 协议层的元数据(如 method name、seq id、compress flag),不是 TCP 层的 length prefix —— length prefix 是传输层的事,应封装在 WriteRequest 底层,对上层 Codec 透明:
-
WriteRequest内部:先json.Marshal(req)得到payload,再binary.BigEndian.PutUint32(header[:], uint32(len(payload))),最后conn.Write(append(header[:], payload...)) -
ReadRequestHeader不负责读 length prefix,它只从已解帧的payload中解析 method、seq 等字段(比如用json.Unmarshal到一个临时struct{Method string; Seq uint64}) - 真正的分帧逻辑(读 4 字节 length,再读 length 字节 payload)应抽成独立函数,如
readFrame(conn net.Conn) ([]byte, error),被ReadRequestHeader和ReadRequestBody共用
别在 ReadRequestHeader 里做 io.ReadFull(conn, ...) —— 这会让后续 ReadRequestBody 无数据可读,或触发重复读取。
选 json 还是 protobuf?别只看体积
线上翻车往往不是因为性能差,而是因为语义模糊或演进失控:
-
json:字段名大小写敏感(json:"user_id"vsjson:"userId")、time.Time默认序列化为 RFC3339 字符串(服务端用int64存,客户端却收到字符串)、nilslice 和空 slice 在反序列化后行为不一致 -
gob:Go 专属,跨语言等于零;Go 版本升级可能破坏兼容性(如 1.19+ 对未导出字段编码行为变更);抓包看到的是乱码,无法快速验证 payload -
protobuf:需要定义.proto文件,但带来强 schema 控制、明确的字段编号、向后兼容保障(新增字段默认忽略)、多语言支持;只要不用oneof或嵌套过深,性能不输json
如果你的 RPC 要长期维护、或未来可能接入 Python/JS 客户端,protobuf 不是“更优选项”,而是“唯一合理选项”。json 只适合内部调试或临时脚本。
ServerCodec 必须处理压缩与错误传播
压缩不是锦上添花,而是 Codec 的责任边界之一。如果你在 WriteRequest 里用了 gzip.Writer,就必须保证:
- 每次调用后显式
Flush(),否则数据滞留在缓冲区,连接超时断开 - 压缩只作用于 payload(即序列化后的字节),不能包含 length prefix —— 否则接收方先读 4 字节,发现不是真实长度,后续全乱
-
ReadRequestHeader失败时,必须返回具体错误(如io.ErrUnexpectedEOF),不能吞掉或返回nil;否则rpc.Server会静默关闭连接,日志里只显示connection reset by peer - 如果协议支持 compress flag,
ReadRequestBody必须根据 flag 动态选择gzip.NewReader或直通解码,且解压失败要透传错误,不能 fallback 到原始字节
最容易被忽略的一点:WriteResponse 也得走同样压缩路径。否则客户端收到的响应没压缩,而请求压缩了,协议就失衡——尤其当服务端返回大数组时,带宽浪费立刻显现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











