Go context取消信号必须主动监听ctx.Done()并响应退出,否则协程持续占用资源;仅传入context而不检查Done通道无效,关键阻塞点和循环中须select监听。

Go 的 context 取消信号不是“通知一下就算了”,它必须被主动监听、响应并终止协程,否则长连接会持续占用资源,直到进程退出或系统强制回收。
长连接中不监听 ctx.Done() 就等于没传 context
很多开发者把 ctx 传进 goroutine 就以为万事大吉,但只要没在关键阻塞点(比如 conn.Read()、http.Client.Do()、数据库查询)前或循环中检查 ,取消信号就完全失效。
- HTTP 长轮询或 WebSocket 连接里,常见写法是
for { conn.Read(...) },若没在每次读之前加select { case ,哪怕客户端断开、父 context 已 cancel,goroutine 仍卡在系统调用里不退出 -
net.Conn本身不感知 context,必须手动封装:用conn.SetReadDeadline()配合ctx.Deadline(),或改用http.TimeoutHandler等支持 context 的高层封装 - 第三方库(如
pgx、redis-go)多数已支持WithContext()方法,优先用它们,而不是自己套select
WithCancel 和 WithTimeout 在连接生命周期中的选择逻辑
长连接维护场景下,选哪种创建方式,取决于谁控制终止权:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用户主动断连(如前端关闭页面)→ 用
WithCancel,由 handler 显式调用cancel(),比如收到connection close消息后触发 - 心跳超时或服务端保活策略 → 用
WithTimeout或WithDeadline,例如每 30 秒发一次心跳,设置WithTimeout(ctx, 45*time.Second),确保两次心跳间隔内未响应即断连 - 不要混用:一个连接同时用
WithCancel+WithTimeout嵌套没问题,但若子 context 超时取消,父 context 的cancel()再调一次会 panic ——cancel函数只能调一次
协程泄漏的典型表现与定位方法
没正确传播取消信号的长连接,最终表现为 goroutine 数持续上涨,且堆栈停留在 I/O 等待上:
立即学习“go语言免费学习笔记(深入)”;
- 现象:
runtime/pprof抓取 goroutine profile,大量 goroutine 停在net.(*pollDesc).wait或internal/poll.runtime_pollWait - 验证:在连接建立处加日志,打印
ctx.Err(),如果连接断开后该值长期为nil,说明取消未生效 - 陷阱:用
context.WithValue传连接对象本身,却不传 context 到底层读写逻辑 —— value 是附着在 context 上的,但取消能力只来自Done()通道
真正难的不是写对一行 select,而是确保每个可能阻塞的路径都被覆盖;尤其是跨多层封装(如自定义中间件、重试逻辑、流式响应)时,context 容易在某一层被丢弃或替换。

















