Go 的 GC 能正确回收循环引用但不可达的对象;真正导致内存泄漏的是全局缓存、goroutine 泄漏、sync.Pool 未重置指针、HTTP handler 持有长生命周期上下文等强引用路径。
Go 的垃圾回收器(GC)基于三色标记-清除算法,能正确识别并回收存在循环引用但已不可达的对象。你写 struct A { B *B } 和 B { A *A },只要整组对象从根(如全局变量、栈帧)断开,它们就会被回收——不需要手动置 nil 或打破引用链。
真正危险的是:对象图中存在强引用路径,使本该被回收的对象长期存活。常见于:
sync.Pool 存储含指针字段的结构体,且未清空指针字段,造成池中对象间接持有所属资源*http.Request 保存到长生命周期对象(如单例 service
当结构体含指针字段(如 *bytes.Buffer、*strings.Builder),不重置会导致旧缓冲区内容残留,甚至意外延长底层字节数组生命周期。必须显式清空指针字段:
type Parser struct {
buf *bytes.Buffer
data []byte
}
func (p *Parser) Reset() {
if p.buf != nil {
p.buf.Reset() // 清空内容,但不释放底层数组
}
p.data = p.data[:0] // 截断 slice,避免持有旧 backing array
}
var parserPool = sync.Pool{
New: func() interface{} {
return &Parser{buf: &bytes.Buffer{}}
},
// 注意:Go 1.21+ 支持 Pool 的 Reset 方法,但需确保类型实现 Reset()
}
关键点:Reset() 不是 GC 触发条件,而是防止复用时数据污染和隐式内存驻留;sync.Pool 本身不触发 GC,它只是对象复用机制。
不要靠“有没有循环引用”猜泄漏。用以下方式确认:
http.ListenAndServe("localhost:6060", nil),访问 /debug/pprof/heap 下载堆快照,用 go tool pprof 查看 top allocs / inuse_objectsruntime.ReadMemStats(&m),对比 m.Alloc 和 m.TotalAlloc 增量/debug/pprof/goroutine?debug=2
如果你看到某个结构体实例数随请求线性增长,且 pprof 显示其被 globalMap 或 http.serverHandler 直接/间接引用,那才是真问题——和指针是否循环无关,只和引用是否可被 GC 到有关。