长连接必须显式绑定context.context,因其无法自动感知上层取消或超时;若不传ctx或未在读写循环中持续监听ctx.done(),将导致goroutine泄漏、资源堆积和请求堆积。
长连接的生命周期必须由 context 主动控制,不能靠连接自身超时或被动关闭来兜底;否则协程泄漏、资源堆积、请求堆积会立刻发生。
为什么长连接必须显式绑定 context.Context
HTTP/2 连接、gRPC 流、WebSocket、数据库连接池中的活跃连接、自定义 TCP 长连接等,本质都是「可能持续数分钟甚至数小时」的资源。它们不会自动感知上层请求已取消或超时——除非你把 ctx 传进去,并在关键阻塞点监听 ctx.Done()。
- 不传
ctx:连接会一直 hold 住 goroutine,哪怕客户端早已断开、服务端已重启 - 只传一次
ctx但没在读写循环里检查:一次超时后连接仍保持,后续操作继续用这个“僵尸连接” - 用
time.AfterFunc单独关连接:无法与 cancel 信号同步,可能重复关闭或漏关
net.Conn 和 http.Client 怎么接入 context.Context
原生 net.Conn 不支持 context,必须包装或换用支持 context 的封装;http.Client 则需配置 Timeout 或直接传 ctx 到方法调用中。
-
http.Client发起请求时,直接用client.Do(req.WithContext(ctx)),而非client.Do(req) - 自定义 TCP 连接:用
net.Dialer的DialContext方法,而不是Dial - gRPC 客户端:所有 RPC 方法都接受
ctx参数,必须传;流式调用更需在每次Recv()/Send()前检查ctx.Err() == nil - 数据库(如
database/sql):查询方法如db.QueryContext(ctx, ...)、tx.StmtContext(ctx, ...)必须使用带Context的变体
长连接协程里怎么安全退出
典型错误是写一个 for { select { case ,完全忽略 <code>ctx.Done()。正确做法是在每个可能阻塞的位置插入 context 检查。
- 不要用
for range ch接收消息,改用for { select { case msg := - 对
conn.Read(),用conn.SetReadDeadline配合ctx超时,或改用支持 context 的封装(如bufio.NewReader(conn).ReadString('\n')不行,要换成io.ReadFull+ctx超时包装) - 启动协程时,立刻 defer
cancel()(如果该ctx是WithCancel派生的),确保无论从哪条路径退出,都能通知子节点 - 若连接本身支持
Close,务必在case 分支里调用,且避免重复 close(加 <code>sync.Once或状态标记)
容易被忽略的 context 传递断层
长连接常嵌套在多层结构中:比如一个 WebSocket handler 启动了心跳 goroutine,心跳又启动了重连 goroutine。一旦某一层没把 ctx 传下去,那一层就彻底脱离生命周期管理。
- 所有中间函数签名必须显式接收
ctx context.Context,不能藏在 struct 字段里 - 不要在 struct 初始化时存
ctx,因为 struct 生命周期往往比请求长;应每次方法调用都传入最新ctx - 日志、metrics、trace ID 等上下文数据,用
context.WithValue传,但 key 必须是私有类型(type ctxKey string),且只传小而必要字段 - 调试时,可在关键节点打日志:
log.Printf("conn %p closing: %v", conn, ctx.Err()),确认是否真被 cancel 触发
最危险的情况不是 context 没传,而是传了但没在 IO 阻塞点检查——它看起来工作正常,直到压测时 goroutine 数暴涨到几千,pprof 显示全卡在 read 或 select 上。











