goroutine 泄漏是指一个永远无法终止的 goroutine——它被阻塞或永远运行,在程序的整个生命周期内消耗内存(并保持其引用的对象活跃)。由于 goroutine 启动成本低廉,很容易泄漏它们,泄漏会无声地积累,直到服务性能下降或内存耗尽。
原因 1:在没有发送者/接收者的 channel 上阻塞
{
ch := ( )
{
ch <-
}()
}
在 channel 的发送/接收操作上阻塞且永不完成的 goroutine 将永远等待——它永远不会被垃圾回收,因为在技术上它仍然"在运行"。
// ❌ LEAK — a goroutine in an infinite loop with no way to stop
func worker(jobs <-chan int) {
for {
job := <-jobs // blocks forever if jobs is never closed and stops sending
process(job)
} // no exit condition — leaks when no longer needed
}
// ✅ the goroutine can be told to stop
func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case job := <-jobs:
process(job)
case <-ctx.Done(): // cancellation signal → exit cleanly
return // the goroutine terminates, no leak
}
}
}
// caller: ctx, cancel := context.WithCancel(...); defer cancel()
为每个长生命周期的 goroutine 提供被取消的方式(<-ctx.Done())是主要防御措施——它确保当不再需要 goroutine 的工作时,goroutine 能够终止。
// ✅ ensure a receiver exists, or use a buffered channel so the send doesn't block
ch := make(chan int, 1) // buffered → the send completes even if no one receives yet
go func() { ch <- 42 }() // doesn't block
// ✅ close channels so range loops terminate
go func() {
defer close(results) // signal completion → for-range over results ends
for _, job := range jobs { results <- process(job) }
}()
runtime.NumGoroutine() // monitor the goroutine count — steady growth = leak
import _ "net/http/pprof" // pprof exposes goroutine stacks (/debug/pprof/goroutine)
// the goleak library asserts no leaked goroutines in tests
监控 runtime.NumGoroutine()(数量持续上升表示泄漏)、通过 pprof 检查 goroutine 转储和在测试中使用 goleak 是主要的检测工具。
Goroutine 泄漏是生产环境中 Go 服务最常见且最隐蔽的问题之一。
因为 goroutine 启动成本极低,开发者会自由地创建它们——但是被永远阻塞的 goroutine(在没有对端的 channel 上,或在没有取消机制的无限循环中)永远不会终止,也永远不会释放其内存(包括它引用的所有对象)。
在长运行的服务器中,这些泄漏会无声地积累,逐渐消耗内存,直到服务性能下降或崩溃——它们很难被发现,因为没有立即的错误提示。
理解原因(阻塞的 channel 操作、缺少取消机制)和防止方法(基于 context 的取消作为主要防御、正确的 channel 关闭、带缓冲的 channel、确保接收者存在)对于编写可靠的并发 Go 代码至关重要。
掌握检测工具(runtime.NumGoroutine、pprof、goleak)同样重要。
这是一个关键的、经常被考察的话题,它区分了能够编写生产级并发 Go 代码的开发者和在持续负载下会出现泄漏的开发者。