conn.read() 卡住非 bug,是因未设 setreaddeadline 导致永久阻塞;高并发下易引发 goroutine 积压和资源泄漏,须每次读前重置超时,http/1.1 keep-alive 可一次设置;写操作需用带缓冲 channel 解耦,避免竞争和阻塞。

conn.Read() 卡住不是 bug,是没设 SetReadDeadline
Go 的 conn.Read() 默认会永久阻塞,直到有数据、连接关闭或出错。高并发下若客户端不发数据也不断连,成千上万个 goroutine 就会停在 net.(*conn).Read,pprof 看起来像“卡死”,实则是健康挂起——但前提是它真在等;一旦没超时机制,就变成资源泄漏。
必须每次调用 conn.Read() 前重置读超时:
-
conn.SetReadDeadline(time.Now().Add(30 * time.Second))—— 空闲 30 秒无数据即断连 - 别复用旧时间值,否则 deadline 越来越早,连接提前被杀
- 错误类型是
*net.OpError,用os.IsTimeout(err)判断,不是字符串匹配"i/o timeout" - HTTP/1.1 keep-alive 场景下,一次设置覆盖整个连接周期,无需每读一次都设(但得确保首次设置成功)
写 Goroutine 不要直接 conn.Write(),用带缓冲 channel 解耦
多个 goroutine 同时调用 conn.Write() 会竞争底层 socket 状态,导致 panic 或数据错乱。标准解法是:一个 goroutine 专读,一个专写,中间用 channel 传数据。
但 channel 若无缓冲,写 goroutine 在远端接收慢时会阻塞,进而拖垮读 goroutine(因读 goroutine 发送失败后可能 panic 或丢包)。
- 用带缓冲的
chan []byte,容量建议 ≥ 16(根据消息平均大小和突发流量调整) - 读 goroutine 收到完整消息后,
select { case writeChan 主动丢弃,避免阻塞 - 写 goroutine 从 channel 取数据后,立刻调用
conn.Write(),并设conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - 写完记得
if n != len(data) { ... }检查是否写全,未写全需重试或记录错误
限速器不能只靠计数,得配合 context 和 channel select
纯计数型限速(如每秒最多 100 次 Write())在高并发下容易失效:goroutine 涌入快于消费速度,channel 积压,内存涨,延迟飙升。
真正可控的限速,是让“过载请求”在源头就放弃,而不是排队等死。
- 用
time.Ticker配合select实现平滑限速:select { case - 对写操作加
context.WithTimeout(ctx, 2*time.Second),防止单次写卡太久 - 不要全局共享一个限速器实例;每个连接应持有独立限速状态(如
rate.Limiter),避免锁争用 - 若用
golang.org/x/time/rate,注意Limiter.WaitN()会阻塞,而Limiter.AllowN()+select更适合非阻塞场景
别碰原始 fd,Keep-Alive 交给 *net.TCPConn 自己管
想开 TCP keepalive?别用 github.com/felixge/tcpkeepalive 这类直接操作 socket fd 的库。它会让 goroutine 绑定 OS 线程,连接数一过万就触发 runtime: program exceeds 10000-thread limit。
Go 标准库的 *net.TCPConn 已内置支持:
- 监听后对 listener 调用
ln.(*net.TCPListener).SetKeepAlive(true) - accept 到 conn 后,转成
*net.TCPConn并启用:tcpConn.SetKeepAlive(true),tcpConn.SetKeepAlivePeriod(30 * time.Second) - 全程不碰
syscall,不调setsockopt,完全走 Go runtime 的 netpoller 路径 - Linux 3.9+、Go 1.11+ 才支持
SetKeepAlivePeriod,旧版本只能设开关,周期由内核默认(通常 2 小时)
最易被忽略的点:每个连接的读、写、超时、限速、心跳,必须共用同一个 context.Context 生命周期。一旦某处漏了 ctx.Done() 检查(比如日志写入、metric 上报),那个 goroutine 就永远 hang 在那里,不响应断连、不释放内存、不退出。这不是并发问题,是生命周期管理失格。











