goroutine泄漏比内存泄漏更常见且难发现,表现为程序卡顿、cpu偏高、runtime.numgoroutine()持续上涨;主因是goroutine阻塞在channel接收、sleep、锁或未关闭http连接且无退出路径。

goroutine 泄漏比内存泄漏更常见,且更难发现
Go 程序卡顿、CPU 持续偏高、runtime.NumGoroutine() 持续上涨,八成是 goroutine 泄漏,不是内存泄漏。根本原因在于:goroutine 启动后若阻塞在 channel 接收、time.Sleep、锁等待或未关闭的 HTTP 连接上,又没有退出路径,就会永久存活。
实操建议:
- 用
pprof抓取/debug/pprof/goroutine?debug=2,看堆栈里大量 goroutine 停留在哪一行(尤其关注select无默认分支、channel 无人接收、HTTP client 超时未设) - 所有带超时的 I/O 操作必须显式设置:HTTP client 要配
Timeout、Transport.IdleConnTimeout;数据库连接要用context.WithTimeout - 启动 goroutine 前,问自己:它怎么结束?谁负责 close channel?有没有可能永远等下去?
map 并发写入 panic 不一定当场暴露
fatal error: concurrent map writes 是运行时 panic,但 Go 不保证每次并发写都立即触发——它依赖竞争检测器(race detector)或调度器偶然调度重叠。线上环境关了 -race,就可能跑几天才崩,或只在高负载下复现。
实操建议:
- 凡跨 goroutine 读写同一
map,必须加sync.RWMutex或改用sync.Map(注意:sync.Map仅适合读多写少,且不支持遍历中删除) - 初始化时就确定 key 集合?直接用
make(map[K]V, n)+ 读写锁;动态增删频繁?考虑用sharded map或第三方库如github.com/orcaman/concurrent-map - 本地开发务必开启
go run -race,CI 流水线也应加入 race 检查步骤
defer 在循环里闭包捕获变量容易出错
写 for i := range items { defer func() { log.Println(i) }() },最后所有 defer 都打印最后一个 i 的值——因为匿名函数捕获的是变量地址,不是值快照。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
实操建议:
- 立刻修复方式:把循环变量传进闭包,
defer func(idx int) { log.Println(idx) }(i) - 更安全的习惯:循环体中避免在 defer 里引用外部循环变量;如需延迟执行,优先提取为独立函数并显式传参
- 注意
defer执行顺序是 LIFO,嵌套循环 + defer 容易导致资源释放顺序错乱(比如先 close file 再 close zip writer),要手动控制释放时机
HTTP server 没设 ReadHeaderTimeout / IdleTimeout 就等于没设超时
Go 1.8+ 默认不设任何超时,http.Server 可能被慢速客户端拖住数小时:一个只发一半请求头的连接,会一直占着 goroutine 和文件描述符,最终耗尽 MaxOpenFiles。
实操建议:
- 至少配置三项:
ReadHeaderTimeout(防止慢请求头)、ReadTimeout(防止慢 body)、IdleTimeout(防止长连接空闲占用) - 不要只依赖
WriteTimeout——它只管响应写出,不管请求进来卡在哪 - 用
net/http/httputil.ReverseProxy作反向代理时,同样要给Director和后端 transport 设超时,否则上游超时了,下游还在等
真正棘手的泄漏往往藏在「看起来很安全」的地方:比如用 io.Copy 转发 HTTP body 却忘了检查返回错误,导致连接永不关闭;或者用 context.WithCancel 后忘了调用 cancel 函数,让整个子树 goroutine 无法回收。排查时别只盯着 pprof/heap,先看 goroutine 数和阻塞点——多数时候,问题不在内存,而在控制流没走完。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










