真正内存泄漏表现为heapinuse持续单向增长且pprof heap中某类对象数量/大小线性上升;goroutine泄漏最常见,多因channel阻塞、无超时select等导致长期驻留。

用 pprof 发现真实内存泄漏点
Go 程序里很多“内存涨得慢”不是泄漏,而是缓存没控制、对象复用不足或 GC 未及时触发;真正泄漏的典型特征是 runtime.MemStats.HeapInuse 持续单向增长,且 pprof heap 中某类对象数量/大小随时间线性上升。
启动时加 net/http/pprof 是最轻量方式:
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... 其他逻辑
}
运行后访问 http://localhost:6060/debug/pprof/heap?debug=1 看原始统计,或用 go tool pprof http://localhost:6060/debug/pprof/heap 进入交互式分析,重点关注:
-
top -cum找调用链顶端 -
web生成调用图(需 Graphviz) -
list <funcname></funcname>定位具体行号分配点
goroutine 泄漏是最常见根源
泄漏的 goroutine 往往卡在 channel receive、time.Sleep、或无缓冲 channel send,它们不退出,连带持有的栈、闭包变量、引用的对象全无法回收。
立即学习“go语言免费学习笔记(深入)”;
检查方法:
- 访问
http://localhost:6060/debug/pprof/goroutine?debug=2查看所有 goroutine 栈帧 - 用
go tool pprof http://localhost:6060/debug/pprof/goroutine后执行top,看是否大量 goroutine 停留在同一函数(如select或recv) - 特别注意
for range chan未关闭 channel、select缺少default或timeout的长期阻塞场景
示例问题代码:
func badWorker(ch <-chan int) {
for v := range ch { // ch 永不关闭 → goroutine 永不退出
process(v)
}
}
map 和 sync.Map 的误用导致键值残留
普通 map 不会自动清理旧键,如果用时间戳、请求 ID 等作为 key 且永不删除,map 本身和 value 引用的对象都会持续占内存;sync.Map 虽并发安全,但同样不会自动驱逐。
修复思路:
- 明确 map 生命周期:用
make(map[KeyType]ValueType, 0)初始化,并在作用域结束前清空(for k := range m { delete(m, k) }) - 需要缓存时,优先考虑带 TTL 的方案,比如
github.com/bluele/gcache或自己封装定时清理 goroutine - 避免把大结构体直接塞进 map value —— 改用指针或 ID 引用,降低 map 自身膨胀速度
尤其警惕日志、监控、trace 上下文里临时拼接的 map,容易被遗忘。
io.Reader / http.Response.Body 忘记 Close
这是 Go 新手高频踩坑点:只要用了 http.Get、http.Post、os.Open 或任何返回 io.ReadCloser 的 API,就必须显式调用 Close(),否则底层连接、文件句柄、buffer 内存全部泄漏。
正确写法永远带 defer resp.Body.Close()(且必须在检查 err 后):
resp, err := http.Get("https://example.com")
if err != nil {
return err
}
defer resp.Body.Close() // ← 这行不能少,也不能写在 err 判断前
body, _ := io.ReadAll(resp.Body)
// ...
其他易漏场景:
-
sql.Rows忘记rows.Close() -
bufio.Scanner读完未检查Err()导致底层 reader 持有 buffer - 自定义
io.Reader实现中,Read方法返回io.EOF后仍被反复调用且不释放资源
这类泄漏往往表现为 goroutine 数稳定但内存缓慢上涨,因为底层连接池或 buffer 持续堆积。
真正难定位的是跨包、跨模块的隐式持有:比如一个全局 logger 持有 context、一个 metrics collector 缓存了 request 对象指针、或者第三方库内部启了 goroutine 却没暴露 shutdown 接口。这时候得靠 pprof 的堆对象类型 + 采样地址反查,而不是猜。










