Go的net.Conn只处理字节流,故conn.Write(struct)必报错;必须先序列化为[]byte,再添加长度前缀(如4字节大端长度头)以解决TCP粘包问题。

Go 的 net.Conn 本身不认“消息”,只认字节流;你直接 conn.Write(myStruct) 必然报错 cannot use myStruct (type MyStruct) as type []byte。真正能上线的自定义协议,必须同时解决两件事:序列化 + 消息边界。缺一不可。
为什么 conn.Write(struct) 一定失败
因为 net.Conn 接口只实现了 Write([]byte),而 Go struct 没有隐式转 []byte 的机制。常见错误写法包括:
- 直接传 struct:
conn.Write(myData)→ 编译报错 - 用
fmt.Sprintf("%v", myData)拼字符串再转[]byte→ 字段顺序、空值、时间格式全不可控 - 用
json.Marshal(myData)后直接Write()→ 解决了序列化,但没解决粘包,接收方读不到完整 JSON
核心原则:先 Marshal 成 []byte,再按协议规则封装(比如加长度头),最后 Write()。
怎么加长度前缀解决 TCP 粘包
最简可靠方案是「4 字节大端长度 + payload」。这不是可选项,而是 TCP 流式特性的硬性要求。接收方必须先读够 4 字节,才知道接下来该读多少。
立即学习“go语言免费学习笔记(深入)”;
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 写包用
binary.BigEndian.PutUint32(header, uint32(len(payload))),别用int—— 跨平台位宽不一致 - 读包必须用
io.ReadFull(conn, header[:]),不能用裸conn.Read(),否则可能只读到 2 字节就返回 - 长度字段本身不能压缩或加密,否则无法预判后续读取量
- 建议设上限(如
if length > 1024 * 1024 { return nil, ErrTooLarge }),防 OOM
示例解包逻辑:
func readPacket(conn net.Conn) ([]byte, error) {
var lengthBuf [4]byte
if _, err := io.ReadFull(conn, lengthBuf[:]); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(lengthBuf[:])
payload := make([]byte, length)
if _, err := io.ReadFull(conn, payload); err != nil {
return nil, err
}
return payload, nil
}选什么序列化格式才不至于线上翻车
别用 gob 做跨语言通信,也别无脑上 json 就以为万事大吉。
-
gob是 Go 专属,Python/PHP 客户端根本解不开;Go 版本升级或结构体字段重排,旧客户端直接panic -
json在线上常因字段大小写、time.Time默认序列化格式、浮点精度、nilvs""处理模糊而失败,典型错误:json: cannot unmarshal string into Go struct field X.Time - 生产环境推荐
protobuf(强 schema、多语言、版本兼容)或msgpack(轻量、多语言,但需自行约定 schema 演进) - 如果只是内部服务且纯 Go,
gob可用,但必须保证服务端/客户端 Go 版本一致,且结构体字段顺序、导出状态严格一致
解包时 goroutine 卡死或泄漏的常见原因
不是协议写得不对,而是资源控制没跟上。
- 没设
conn.SetReadDeadline()→ 网络异常时ReadFull()永久阻塞,goroutine 泄漏 - 粘包处理没剥离剩余字节 → 一次读到两个包,第二个包的开头被当成了新包的长度头,解析失败后无限等待
- 每次解包都
make([]byte, length)→ 小包高频分配触发 GC,吞吐掉一半 - 错误长度(如超 1GB)没立即断连 →
ReadFull会等超时,期间占用连接和 goroutine
真正健壮的解包函数应返回 ([]byte, []byte, error):第一个是完整消息,第二个是本次读取中未消费的剩余字节,用于下一次迭代 —— 这才是处理粘包的正解。

















