goroutine 泄露表现为内存持续增长且 pprof 显示大量阻塞在 select 或 chan receive 的 goroutine;确认方法是调用 /debug/pprof/goroutine?debug=2 并检查堆栈中 goroutine 数量是否随请求线性增加。

goroutine 泄露的典型现象和快速确认方法
程序内存持续增长、pprof 查看 runtime/pprof/goroutine profile 显示大量处于 select 或 chan receive 状态的 goroutine,基本可断定存在泄露。常见诱因是启动了 goroutine 却没给它提供退出信号——比如用 time.After 做超时但没配合 context.WithTimeout,或向无缓冲 channel 发送后没人接收,导致 sender 永久阻塞。
验证是否泄露:运行时执行
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2',搜索
goroutine 1 [chan send]: 或 goroutine X [select]: 这类堆栈,若数量随请求线性增加,就是泄露。
用 context.WithCancel / WithTimeout 正确控制 goroutine 生命周期
不能只靠 defer cancel() 就以为万事大吉——cancel 函数必须被调用,且 goroutine 内部必须监听 ctx.Done() 并主动退出。漏掉任一环节都会失效。
- 启动 goroutine 前必须传入 context,且该 context 应由调用方创建(如
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)) - goroutine 内部要用
select监听ctx.Done(),并在分支中 return,不能只打印日志或忽略 - 不要在 goroutine 内部再调用
context.WithCancel(ctx)创建子 context 后忘记调用 cancel —— 子 cancel 不被调用,父 ctx.Done() 关闭时子 goroutine 仍可能存活
错误示例(泄露):
go func() {
time.Sleep(10 * time.Second) // 没监听 ctx,超时后仍运行
doWork()
}()
正确写法:
go func(ctx context.Context) {
select {
case <-time.After(10 * time.Second):
doWork()
case <-ctx.Done():
return // 必须显式退出
}
}(parentCtx)
channel 操作与 context 结合的三个关键点
goroutine 泄露常发生在 channel 通信场景:发送者卡住、接收者没启动、或 select 中 default 分支掩盖了阻塞风险。
立即学习“go语言免费学习笔记(深入)”;
- 向无缓冲 channel 发送前,确保有 goroutine 在另一端接收;否则应使用带缓冲 channel 或改用
select+ctx.Done()防守 - 用
select读写 channel 时,case 和case ch 都必须配case ,不能只加一个 - 避免滥用
default:它会让 channel 操作非阻塞,看似不卡,实则丢数据且掩盖真实流程异常
安全发送示例:
select {
case ch <- data:
// 发送成功
case <-ctx.Done():
// 上下文取消,不强求发送
return
}
测试阶段如何暴露 goroutine 泄露
单元测试里启动 goroutine 后不 cancel,是泄露高发区。Go 自带的 test 包提供 TestMain 和 runtime.NumGoroutine() 可辅助检测。
- 每个测试函数开头记录 goroutine 数量:
before := runtime.NumGoroutine() - 测试逻辑结束后,加
time.Sleep(10ms)让 goroutine 有机会退出,再检查:if runtime.NumGoroutine() > before + 2 { t.Fatal("goroutine leak detected") } - 对 HTTP handler 测试,用
httptest.NewServer启服务时,务必调用server.Close(),否则其内部 listener goroutine 会残留
注意:runtime.NumGoroutine() 是粗粒度指标,只能发现明显泄露;真正定位要靠 pprof + 人工堆栈分析。
context 不是银弹,它只提供信号通道;真正决定 goroutine 是否退出的,是你在 select 里写了什么、有没有 return、channel 缓冲是否匹配、以及 cancel 函数是否被调用。漏掉其中任意一环,泄露就藏在那行没被执行的 return 后面。










