go原生net包可支撑高并发长连接,但需正确处理四点:accept后立即启goroutine、设setreaddeadline实现心跳、读写分离至不同goroutine、conn.close()须待读写协程均退出后再执行。

Go 原生 net 包完全能支撑高并发长连接,但直接裸用 net.Conn 极易在生产环境崩掉——不是性能不够,而是四个关键点没设对:Accept 后不启 goroutine、不设 SetReadDeadline、读写混在一个协程、conn.Close() 时机错乱。
Accept 后必须立刻启 goroutine,不能在主循环里 Read
常见错误是把 conn.Read() 写在 for { conn, _ := listener.Accept() } 主循环里。一个客户端卡住(比如发半包、不发数据),整个 Accept 循环就堵死,新连接进不来。
- 正确做法:每次
listener.Accept()成功后,**立即**go handleConn(conn) -
handleConn函数里只做该连接的事,不碰 listener - 主循环里 **不要** 出现任何
conn.Read()或conn.Write() - 哪怕只是 echo,也要起 goroutine;并发量上来后,这点开销远小于阻塞代价
必须用 SetReadDeadline 做心跳,别信 SetDeadline
TCP 层没有心跳机制,NAT、云 LB、家用路由器普遍 5–30 分钟空闲就静默断连。等 conn.Read() 返回 EOF 或 io.EOF 才关连接,已经晚了——客户端可能早就掉线,服务端还留着“幽灵连接”占内存和 fd。
- 只调
conn.SetReadDeadline(),**别用**conn.SetDeadline()——后者同时影响读写,会误杀业务写操作 - 每次
conn.Read()前设置:conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 收到有效数据(哪怕只是
"ping")后,**立刻重置** deadline,形成应用层心跳 - 若
err != nil且err.(net.Error).Timeout()为 true,说明读超时,大概率客户端已失联,应主动return让 defer 关闭
读写必须分离到不同 goroutine,否则粘包+阻塞一起爆
把 Read 和 Write 放同一个 goroutine,看似简单,实则埋雷:写操作慢(比如序列化耗时、下游调用阻塞)会导致读超时被误判;更麻烦的是,多个 Write 并发调用 conn.Write() 会因 TCP 缓冲区竞争导致粘包错乱。
- 每个连接至少起两个 goroutine:
go readLoop(conn)和go writeLoop(conn) -
readLoop负责收包、解包、投递到内部 channel;writeLoop从 channel 取包、封包、发送 - 写操作必须加锁或串行化,例如用
sync.Mutex包裹conn.Write(),或让 writeLoop 独占写通道 - 解包逻辑(如按长度头、分隔符)必须在
readLoop里完成,不能丢给业务层拼接
conn.Close() 必须等读写都结束,defer 不是万能的
很多人在 handleConn 开头写 defer conn.Close(),以为万事大吉。但若读 goroutine 还在跑,conn.Close() 会中断它,触发大量 use of closed network connection 错误;更糟的是,写 goroutine 可能还在往已关闭的 conn 写,panic 或静默失败。
- 关闭前要协调读写两个 goroutine 的退出:可用
sync.WaitGroup+done chan struct{}通知 - 读 goroutine 收到 EOF 或 timeout 后,应 close(done);写 goroutine 检测到 done 关闭就停止取包
-
conn.Close()应放在所有 goroutine 退出之后,或由一个“守门 goroutine”统一执行 - 连接对象本身建议封装状态字段(如
isClosed bool),Send()方法先检查再写,避免竞态
真正难的不是启动多少 goroutine,而是让每个连接的生命周期可控:从 Accept 到 Close,deadline 怎么设、读写怎么分、关闭信号怎么传——这些细节一旦漏掉一环,压测时连接数上不去,线上就慢慢堆积泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











