直接用net.conn读写会丢包或粘包,因为tcp是字节流协议,不保证消息边界:一次write可能被合并(粘包)或拆分(半包),必须在应用层定义长度前缀协议(如4字节大端序长度头),配合io.readfull和bytes.buffer滚动解析,否则必然导致解析错位、panic或goroutine泄漏。

为什么直接用 net.Conn 读写会丢包或粘包
TCP 是字节流协议,不保证消息边界。你调用一次 conn.Write() 发 100 字节,对方 conn.Read() 可能只读到前 32 字节,也可能一次读到连续发的两条消息——这就是粘包和半包。自定义协议必须自己拆包,不能依赖“一次 Write 对应一次 Read”。
常见错误是:在循环里无条件 conn.Read(buf),然后直接解析 buf[:n],结果要么阻塞等待、要么解析错位、要么 panic 切片越界。
- 必须定义明确的帧头(比如前 4 字节为 uint32 消息长度)
- 读取时先读够帧头,再按长度读取有效载荷
- 使用
io.ReadFull()替代裸Read(),避免手动处理 partial read
怎么用 binary.Read() 安全解析定长头部
假设你的协议是:[4-byte length][payload],头部用大端序(network byte order),这是最简但足够可靠的方案。别手写位移运算,用标准库更稳。
错误做法:用 buf[0:4] 转 uint32 时不考虑字节序,或没检查 len(buf) 就切片。
- 先分配 4 字节缓冲区,调用
io.ReadFull(conn, headerBuf) - 再用
binary.BigEndian.Uint32(headerBuf)解出长度 - 如果长度超限(比如 > 1MB),立刻断连,防内存耗尽
headerBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, headerBuf); err != nil {
return err // 不是 EOF 就该记录日志
}
msgLen := binary.BigEndian.Uint32(headerBuf)
if msgLen > 1024*1024 {
conn.Close()
return errors.New("message too large")
}
如何避免 goroutine 泄漏导致服务假死
每个连接启一个 goroutine 处理很常见,但若客户端异常断开、或读写卡住,goroutine 可能永远阻塞在 Read() 或 Write() 上,累积多了就 OOM。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
核心不是加超时,而是让超时可取消、且清理干净。
- 给每个连接绑定独立的
context.WithTimeout(),传给所有 I/O 操作 - 用
conn.SetReadDeadline()+conn.SetWriteDeadline()配合 timeout,比纯 context 更精准(尤其对底层 socket) - 务必 defer
conn.Close(),并在 goroutine 结束时显式 cancel context
注意:不要只设一次 deadline——每次 Read/Write 前都要重置,否则第二次读就会因过期而失败。
协议升级时如何兼容旧客户端
上线后改协议字段或长度,旧客户端连上来直接解析失败,可能静默断连。需要在帧头里预留版本号字段,哪怕初期只用 1 字节。
错误做法:把版本号藏在 payload 里——等你读完整个 payload 才发现版本不对,浪费带宽和时间。
- 帧头扩展为
[1-byte version][4-byte length][payload] - 先读 5 字节,解析 version,再决定后续如何读 length 和 payload
- 对未知 version 直接返回错误码并关闭连接,不尝试解析
真正麻烦的是 payload 结构变化(比如字段重排序、新增可选字段)。这时建议:旧版用 struct tag 标记 json:"field_name,omitempty",新版加新字段并保持旧字段位置不变,用反射或 codec 库做柔性解码——但前提是头部版本控制已就位。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










