net.Conn不能直接发结构体,因其只处理字节流,需先序列化(如json/gob/二进制)并添加长度前缀以解决TCP粘包问题。

为什么 net.Conn 不能直接发结构体
Go 的 net.Conn 只认字节流,你传一个 struct 过去,它会直接调用 fmt.Sprintf 或者 panic —— 因为没实现 io.Writer 接口。常见错误是写 conn.Write(myStruct),报错:cannot use myStruct (type MyStruct) as type []byte in argument to conn.Write。
真正要做的,是把结构体序列化成字节,再加协议头(比如长度字段),否则接收方根本不知道哪段是哪段数据。
- 必须手动序列化:用
encoding/gob、json.Marshal或二进制编码(如binary.Write) - 必须带长度前缀:TCP 是流式协议,
Read可能只读到半个包,不带长度或分隔符就无法拆包 - 别用
gob跨语言通信:它 Go 专属,PHP/Python 客户端解不开
怎么写一个带长度头的 TCP 包封装函数
最简可用的自定义协议,就是「4 字节大端长度 + 原始 payload」。接收方先读 4 字节,知道接下来该读多少,再读完才解析。
示例中用 binary.Write 写长度头,避免字节序混乱;用 io.ReadFull 替代裸 Read,防止读不满:
立即学习“go语言免费学习笔记(深入)”;
func writePacket(conn net.Conn, data []byte) error {
header := make([]byte, 4)
binary.BigEndian.PutUint32(header, uint32(len(data)))
_, err := conn.Write(append(header, data...))
return err
}
func readPacket(conn net.Conn) ([]byte, error) {
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(header)
payload := make([]byte, length)
if _, err := io.ReadFull(conn, payload); err != nil {
return nil, err
}
return payload, nil
}
-
io.ReadFull是关键:它确保读够指定字节数,不会因 TCP 拆包返回部分数据 - 长度字段建议用
uint32(最大 4GB),别用int:跨平台时int可能是 32 或 64 位 - 如果协议要支持心跳或小包合并,得在应用层做缓冲,不能依赖单次
Write对应单次Read
JSON 协议在生产环境里为什么容易翻车
用 json.Marshal + 长度头看似简单,但实际线上常出问题:字段名大小写不一致、空值处理模糊、时间格式歧义、浮点精度丢失。
典型错误现象:json: cannot unmarshal string into Go struct field X.Time of type time.Time,或者客户端收到 "created_at":"2024-01-01T00:00:00Z",但 Go 解析失败。
- 时间字段必须显式实现
MarshalJSON/UnmarshalJSON,别靠默认行为 - 所有结构体字段加
json:tag,且首字母大写(否则json包看不到私有字段) - 别让
nil指针字段参与 JSON 编码:要么用omitempty,要么初始化为零值 - 性能上,JSON 比二进制编码慢 3–5 倍,高频小包场景(如游戏状态同步)慎用
如何让协议支持向后兼容(比如加字段)
上线后改结构体字段,老客户端立刻 panic。Go 没有运行时 schema 检查,全靠你控制反序列化逻辑。
核心原则:接收方永远按“最小可用字段集”解析,新字段设默认值,缺失字段不报错。
- 用
json.Decoder.DisallowUnknownFields()是双刃剑:开发期能抓错,但一加字段就炸,线上慎开 - 新增字段必须是可选的(指针或带
omitempty的非指针类型),并提供合理默认值 - 如果协议要长期演进,建议用
protobuf+gRPC:字段编号机制天然支持增删,且生成代码强制处理缺失逻辑 - 别在协议里嵌套 map[string]interface{}:它绕过类型检查,debug 时只能靠 log 猜字段含义
协议不是越通用越好,而是越贴近业务边界越稳。字段命名、长度限制、超时策略这些细节,往往比序列化方式更决定线上稳定性。


















