不能直接用 conn.read 解析业务消息,因其返回原始字节流,不保证完整包;需用4字节大端长度头协议,配合io.readfull循环处理缓冲区剩余字节,并校验长度防oom。

为什么不能直接用 conn.Read 解析业务消息
因为 conn.Read 返回的是原始字节流,不保证一次读到一个完整业务包——它只按内核 socket 缓冲区当前有多少数据就返回多少。你看到的可能是:半个包 + 两个整包 + 另半个包。如果直接拿 json.Unmarshal 或 binary.Read 去解析,大概率报 invalid character、unexpected EOF 或字段全零。
常见错误写法:
- 收到数据就立刻
json.Unmarshal(buf, &v),忽略缓冲区里还剩半条消息 - 用
bufio.Scanner配合ScanLines,但业务数据含换行(如用户评论、JSON 字段值),导致提前截断 - 只调用一次
conn.Read就认为“这是一条消息”,没做长度校验和剩余字节处理
必须用 4 字节大端序长度头协议
这是目前最稳、跨语言兼容性最好、能覆盖所有粘包/拆包场景的方案。头部用 uint32 表示后续 payload 长度,接收端先读 4 字节 → 解出 n → 再读满 n 字节。
关键点:
- 头部必须用
binary.BigEndian.PutUint32,不能用本地字节序,否则 Go ↔ Java/Python 通信会错乱 - 发送时别分开写 header 和 body:
io.WriteFull(conn, header)必须成功,再io.WriteFull(conn, body),否则接收方卡在读头阶段 - 接收端两次都必须用
io.ReadFull:一次读头,一次读体;用conn.Read可能只读到 2 字节就返回,后续全乱 - 长度字段用
uint32,不是int32——长度不可能为负,且int32在 32/64 位系统下行为不一致
解包逻辑必须循环处理缓冲区剩余字节
一次 conn.Read 可能带回多个完整包加半包,不能读完就丢缓冲区。必须用 *bytes.Buffer 或切片游标暂存未消费字节,并持续切分。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
典型结构:
- 每次从连接读到新数据后,
buf.Write(data) - 进入
for buf.Len() >= 4循环:先读头 → 检查长度是否合法(比如 >1MB 就断连)→ 若buf.Len() ,跳出等下次数据 - 用
buf.Next(int(msgLen) + 4)拿出完整包,其中前 4 字节是头,后面是 payload - 循环继续,直到缓冲区不够一个头为止
错误做法:每次读完就新建 bytes.Buffer,或把剩余字节直接丢弃——粘包里的后半截永远卡住。
必须设读超时并校验非法长度
没有 conn.SetReadDeadline 的粘包处理,上线后迟早出现 goroutine 泄漏。某个异常连接卡在半包上,会一直占着协程不释放。
建议:
- 在
accept后立即设置:conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 每次成功读一个完整包后重置超时,避免长连接被误断
- 解出长度后立刻检查:
if msgLen > MaxBodySize { conn.Close(); return },别等io.ReadFull超时——防止 OOM 或恶意构造超大长度耗尽内存 - 遇到
io.ErrUnexpectedEOF或io.EOF,应清理连接,不要静默忽略
最容易被忽略的其实是“剩余字节”和“读超时”这两点。它们在测试环境几乎不暴露,但一旦进生产,就是雪崩级故障的起点。










