go原生net包可支撑高并发长连接,但需规避单协程阻塞、空闲断连、粘包错乱、连接泄漏四大坑;关键在于正确实现超时控制、读写分离、心跳保活与优雅收尾。

Go 原生 net 包完全能支撑高并发长连接,但直接裸用 net.Conn 极易在生产环境掉进四个坑:单协程阻塞、空闲断连、粘包错乱、连接泄漏。关键不是换框架,而是把超时、读写分离、心跳、收尾这四件事做对。
Accept 后必须立刻启 goroutine,别在主循环里 Read
常见错误是写成:for { conn, _ := listener.Accept(); handleConn(conn) }——一旦某个 conn.Read() 阻塞或慢,整个 Accept 循环就卡死,新连接进不来。
- 正确做法:
for循环只做Accept,每次成功后立即go handleConn(conn) -
handleConn里不要做耗时同步操作(如 DB 查询),该异步的异步,该加 context 的加 context - 别在
handleConn开头就defer conn.Close()——连接可能还在写响应,提前关会导致write: broken pipe
SetReadDeadline 是保活核心,不是可选项
TCP 层无心跳,NAT、云 LB、防火墙普遍 5–30 分钟空闲即静默断连。现象是某天凌晨 conn.Read() 突然返回 io.EOF 或 read: connection reset by peer,但客户端和服务端都没主动关。
- 每次调用
conn.Read()前,必须设一次conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 若
err != nil且err.(net.Error).Timeout()为 true,说明对端失联或网络中断,应立刻退出读循环并关闭连接 - 收到任何有效数据(含
"ping")后,**必须重置 deadline**,否则下一次读仍会超时 - 别用
SetDeadline()——它同时影响读写,而写通常由业务触发,不该被读超时拖垮
读写必须分离,否则粘包和阻塞互相干扰
把 Read 和 Write 放同一个 goroutine,容易因写阻塞导致读超时误判;也难处理粘包——比如连续两个 Write() 被合并送达,Read() 一次拿到两段消息。
- 每个连接起两个 goroutine:
go readLoop(conn)+go writeLoop(conn) -
readLoop负责接收、解包、投递到业务 channel;writeLoop从 channel 取出消息、封包、Write() - 用
sync.Mutex保护conn.Write()(避免并发写 panic),但别锁整个处理逻辑 - 解包推荐定长头 + TLV(如 4 字节大端长度),比分隔符更可靠,也方便复用
bufio.Reader
连接关闭必须协同,goroutine 泄漏比连接泄漏更致命
常见错误是读 goroutine 检测到 EOF 就 return,但写 goroutine 还在往已关闭的 conn 写,最终 panic 或无限阻塞。
- 用一个
chan struct{}(如exitCh)作为连接生命周期信号 - 读/写 goroutine 都要监听该 channel,任一退出就通知对方停止
-
Stop()方法里先置标志位(如isClosed = true),再 close(exitCh),最后conn.Close() - 写操作前检查
if isClosed { return errors.New("connection closed") },避免向已关连接发数据
真正难的不是启动多少 goroutine,而是让每个连接的读、写、超时、关闭之间形成闭环。哪怕只是 echo 服务,只要没处理好 SetReadDeadline 的重置时机或读写 goroutine 的退出协同,上线一周后就会开始掉连接。细节不在代码行数,而在每一次 Read() 后是否真重置了 deadline,每一次 Write() 前是否确认了连接状态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











