SetReadDeadline仅对下一次Read()生效,需每次成功读取后重置;读写goroutine须配deadline并显式通知退出;连接池应按目标地址隔离;粘包需应用层用固定包头+io.ReadFull处理。

SetReadDeadline 必须每次读成功后重置
很多人以为调一次 SetReadDeadline 就能永久生效,结果连接在第二次 Read() 时直接返回 io.EOF 或阻塞超时断开。根本原因是:它只对**下一次** Read() 生效,不是会话级设置。
- 每次成功读到数据(包括心跳
"ping"、业务包、甚至空字节)后,必须立刻重置 deadline,例如conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 服务端和客户端都要做——客户端发完
"ping"后,必须等收到"pong"再重置;否则无法感知服务端掉线 - 别用
time.Sleep定时发 ping 就完事:没配对的读超时,等于没心跳 - 心跳解析失败不能影响主协议逻辑,建议用独立小 buffer 或预解析包头字段,避免因格式错误导致整个连接被误判为异常
goroutine 泄漏比连接泄漏更致命
每个连接起一个读 goroutine + 一个写 goroutine 是常见模式,但一旦连接已断而写 goroutine 还在往 conn.Write() 发数据,就会卡死并持续占用内存和 fd。
- 写操作必须配
SetWriteDeadline,超时即退出,不能无限制等待 - 读 goroutine 退出时,应通过
chan或context.WithCancel显式通知写 goroutine 停止 - 避免在
defer conn.Close()外部启动写 goroutine——Close()不会自动中断正在阻塞的Write() - 用
sync.WaitGroup跟踪活跃 goroutine 数,重启/关闭时可等待它们自然退出
连接池管理需按目的地址维度隔离
连接池不是全局一把抓,而是针对每个目标 host:port 单独维护。不同 service 的请求可能指向同一 IP+端口,但协议或认证方式不同,混用连接易出错。
- 连接池应由
PoolManager统一管理,按address(如"10.0.1.100:8080")做 key 分池 - 每个池需设最大空闲连接数(如
MaxIdleConnsPerHost=5)、连接存活时间(IdleConnTimeout=60s) - 获取连接时若池为空,应支持阻塞等待或快速失败策略,避免瞬时创建大量新连接打爆 fd 限额
- 连接归还前必须检查是否仍可用(如
conn.RemoteAddr() != nil),不可盲目复用已断连句柄
粘包/分包必须由应用层显式处理
net.Conn 是字节流,不保证消息边界。假设每次 Read() 返回一个完整业务包,是绝大多数长连接 bug 的根源。
立即学习“go语言免费学习笔记(深入)”;
- 推荐用固定包头(如 2 字节 magic + 2 字节 seq + 4 字节 len)+
io.ReadFull拆包,而非依赖bufio.Scanner默认行为 - 接收侧必须循环读直到收满
dataLen字节,中间任何err == io.EOF或超时都应视为连接异常 - 发送侧写入前先写包头再写 payload,且确保两次
Write()不被拆开(必要时加锁或用conn.SetWriteBuffer控制) - 单个连接并发读写不安全,
gorilla/websocket的WriteMessage非线程安全,需加锁或走 channel 序列化
真正难的不是写通连接,而是让成千上万个连接在各种网络抖动、中间设备回收、进程重启场景下,既不泄漏 goroutine,也不堆积无效 fd,还能准确区分“真断连”和“暂时无数据”。这些细节藏在 deadline 重置时机、池 key 构建逻辑、包边界判定条件里,而不是某行代码上。


















