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