Go中无“指针泄漏”概念,泄漏的是因强引用未被回收的内存;nil指针仅在构成根可达路径时防泄漏,如全局缓存中的*User字段需置nil,而局部变量置nil无效。

Go 里没有“指针泄漏”这种独立概念——指针本身不泄漏,泄漏的是它所指向的、本该被回收却因强引用而滞留的内存。
为什么 nil 一个指针有时能防泄漏,有时完全没用?
关键看这个指针是否构成“从根可达”的强引用路径。全局缓存中存了 *User,而 User 又持有 http.Request 的指针?那置 user.CacheRef = nil 就有意义;但如果只是局部函数里忘了置 ptr = nil,而函数早已返回、栈帧销毁,这个操作纯属多余。
- 真正要清空的,是长期存活对象里的指针字段(如全局
map[string]*Config、sync.Pool中复用的结构体) -
defer func() { p = nil }()在短生命周期函数中无效,Go 编译器甚至可能直接优化掉 - 用
runtime.SetFinalizer做兜底?别依赖它——GC 不保证及时调用,且会阻碍对象提前回收
sync.Pool 复用含指针字段的结构体,不 Reset() 就等于埋雷
这是生产环境最隐蔽的泄漏源之一:池中对象反复复用,但 *bytes.Buffer 字段没重置,底层字节数组越积越大;更糟的是,如果该缓冲区曾写入过大 JSON,它背后可能还连着一个未释放的 []byte 底层数组,被整个池间接持有着。
- 必须实现显式
Reset()方法,并在sync.Pool.New返回的对象上确保调用 - 不要只
p.buf.Reset(),还要清空其他指针字段:p.data = p.data[:0]、p.cache = nil - Go 1.21+ 支持 Pool 自动调用
Reset(),但前提是类型实现了该方法且无 panic —— 否则退化为仅调用New
pprof 看到 bytes.makeSlice 占内存高 ≠ 内存泄漏
这是新手最容易误判的点。堆上大量 makeSlice 分配,往往只是业务逻辑正常吃内存(比如批量解析日志、拼接响应体),真正的泄漏藏在它的“持有者”里——你得顺着调用栈往上翻,看是谁在持续 new 出这些 slice 并长期持有。
立即学习“go语言免费学习笔记(深入)”;
- 用
go tool pprof -inuse_space后输入top -cum,关注倒数第二三级函数名(即分配者的调用方) - 对比两个时间点的 heap 快照:
go tool pprof before.out after.out,再用diff -cum找增长最猛的路径 - 如果
*http.Request或*sql.Rows实例数随请求数线性上涨,且被globalHandlerMap或未关闭的chan *Request持有,这才是真凶
最常被忽略的一点:泄漏往往不出现在你写的 struct 里,而出现在你“以为它很轻量”的第三方库对象中——比如一个封装了 net.Conn 和 bufio.Reader 的 client 实例,被塞进 map 当 value 后忘了删,它背后拖着的 TCP 连接、缓冲区、goroutine 全都活下来了。










