http handler 中启动 goroutine 未监听 r.context().done() 是最常见泄漏点:协程不响应连接关闭,卡在写消息、等 channel 或 i/o 上;必须显式传入 context 并在 select 中监听 ctx.done(),配合错误检查(如 io.eof)主动退出。

HTTP handler 里启 goroutine 却没监听连接关闭
这是最常见也最容易被忽略的泄漏点:handler 中用 go func() 启动后台协程,但协程内部既不检查 http.Request.Context().Done(),也不响应连接中断信号。客户端一刷新或关页,goroutine 就卡在写响应、发消息、等 channel 上,永远退不出。
典型错误写法:
func handleChat(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
go func() { // ❌ 没传 ctx,也没监听断连
for msg := range broadcastChan {
conn.WriteMessage(websocket.TextMessage, msg)
}
}()
}
- 必须把
r.Context()传进 goroutine,并在select中优先监听ctx.Done() - WebSocket 场景下,
conn.ReadMessage()和conn.WriteMessage()都会随连接关闭返回websocket.ErrCloseSent或io.EOF,要主动检查并退出 - 别依赖
defer conn.Close()—— 它只关连接,不杀 goroutine;goroutine 必须自己感知断连并 return
使用 gin/echo/fiber 时误复用 context
框架中间件或 handler 里拿到的 context.Context 生命周期和 HTTP 连接绑定,但很多人把它存到全局变量、塞进 map、或传给长期运行的后台任务,结果连接断了,context 已 cancel,但 goroutine 还拿着旧 context 空转。
错误示例(gin):
var globalCtx context.Context // ❌ 全局存 context
func init() {
globalCtx = context.Background()
}
func handleOrder(c *gin.Context) {
globalCtx = c.Request.Context() // 覆盖成当前请求 ctx,但其他 goroutine 可能还在用
go processPayment(globalCtx) // ⚠️ 这个 goroutine 可能刚启动,连接就断了
}
- 每个 goroutine 启动时,必须显式接收自己的
context.Context参数,不能共享或复用外部 ctx 变量 - 若需延长生命周期(比如异步落库),应基于原 ctx 创建子 ctx:
childCtx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second),并在 goroutine 结束时调用cancel() - 框架如 echo 的
c.Request().Context()在连接关闭后会立即触发Done(),这是你唯一可靠的退出信号
WebSocket 连接管理器未做连接健康检查
很多服务用一个全局 *ClientManager 维护所有连接,但只管注册不管清理 —— 客户端网络抖动、强制关闭 tab、NAT 超时断连后,连接对象仍留在 map 里,对应的读/写 goroutine 无人通知退出,持续占用内存和 goroutine。
- 必须为每个连接配对一个心跳机制(如 ping/pong 帧),超时未响应则主动
conn.Close()并从 manager 移除 - 读 goroutine 要捕获
io.EOF和websocket.ErrCloseSent,写 goroutine 要检查conn.WriteMessage()返回 error,任一失败都应触发 cleanup - manager 的
RegisterClient()和UnregisterClient()必须成对出现,且Unregister要显式 close 所有相关 channel、stop ticker、cancel 子 context
pprof 抓不到泄漏?试试这个关键操作
线上看到 runtime.NumGoroutine() 持续上涨,但 /debug/pprof/goroutine?debug=2 里全是 [IO wait] 或 [semacquire],没明显阻塞堆栈?那大概率是连接已断,但 goroutine 还在等 channel 或 timer —— 它们状态是 running 或 runnable,不是 chan receive。
- 先对比两个时间点的 goroutine profile:
curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > before.txt,压测 1 分钟后再抓一次after.txt,用diff before.txt after.txt | grep -E "(goroutine|chan|select)"看新增项 - 重点关注重复出现的函数名 + 行号,比如
chat.(*Client).writePump出现 200 次,基本就是它没退出 - 如果怀疑是 context 传播问题,加一行日志:
log.Printf("ctx done: %v, err: %v", ctx.Done(), ctx.Err()),确认是否真收到 cancel 信号
真正难处理的不是“怎么关”,而是“谁该关、什么时候关、关完是否还有残留”。每次新建 goroutine,都要问自己一句:它的退出路径在哪?有没有人负责通知它停?
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











