内存泄漏主因是goroutine卡住、资源未关闭、缓存未清理;长期运行的goroutine必须用select监听context.Done(),禁止裸写无限for循环。

Go微服务里内存泄漏,90%以上不是堆分配没回收,而是 goroutine 卡住不退出、资源没关、缓存没清——这些根本不会被GC碰,只会让内存持续涨。别等OOM才查,得从写法源头堵死。
goroutine 必须带 context.Done() 退出路径
裸写 go func() { for { doWork(); time.Sleep(1 * time.Second) } }() 是高危操作:一旦 ctx 被 cancel,它照睡不误,栈(至少2KB)、闭包变量、调度器跟踪全卡死。
- 所有长期运行的
goroutine必须进select,且至少含一个分支 - 用
time.Ticker代替time.Sleep,并显式调用ticker.Stop() - HTTP handler 中启异步任务时,传入子
context.WithTimeout(ctx, ...),避免父 ctx 取消后子 goroutine 悬空 - 检查第三方库是否真正支持 context:数据库用
QueryContext,HTTP client 用req.WithContext(ctx)
HTTP 客户端必须复用 + 响应体必须关闭
每次请求都 new *http.Client,或忘了 resp.Body.Close(),连接池会耗尽,net/http.readRequest 分配暴涨,pprof 里一眼可见。
- 全局复用单个
*http.Client实例,不要在函数里反复 new -
Transport必须配置:MaxIdleConns=100、MaxConnsPerHost=10、IdleConnTimeout=90*time.Second - 每条 HTTP 请求后,立刻加
defer resp.Body.Close();若 resp 为 nil,先判空再 close - 别信“body 自动关闭”——标准库只在部分 error 场景下自动关,不可依赖
channel 使用必须明确谁关、何时关、怎么收
向无缓冲 channel 发送、或向满的有缓冲 channel 发送,goroutine 立刻阻塞;receiver 不检查 ok、sender 不关 channel,就是泄漏温床。
立即学习“go语言免费学习笔记(深入)”;
- 坚持单一发送者原则:只有 sender 能
close(ch),receiver 只负责for v, ok := range ch或select+ok判断 - 多 sender 场景下,改用信号 channel(如
stopCh chan struct{}),所有 sender 监听它退出 - 发送前加
select+default防阻塞:select { case ch - receiver 循环里必须处理
!ok和两个退出条件
缓存和全局 map 必须带淘汰机制
var cache = make(map[string]*User) 只增不删,GC 回收不了任何 value——只要 key 还在,整个键值对就钉在堆上,尤其指针 value 会拖垮底层大结构体。
- 禁用裸 map 做缓存;改用
bigcache或freecache,它们自带 LRU 和 TTL - 若用
sync.Map,写入后必须配合定时清理或显式Delete - 避免
globalSlice = bigSlice[:10]:底层数组仍被持有,百万字节白占着 - 所有缓存 key 必须可预测生命周期,比如加时间戳前缀,方便批量过期
最易被忽略的点是:pprof 堆快照单看 inuse_space 没用,得抓两个时间点快照做 diff,才能暴露 bytes.makeSlice 或 runtime.malg 这类高频分配源——它们本身不泄漏,但暴露了你代码里反复 new 的真实问题。


















