什么是栈与堆?Go 的逃逸分析是怎么工作的?
结论先行
栈是每个 goroutine 私有的函数调用内存,自动分配、函数返回即释放、无需 GC;堆是被 GC 管理、生命周期不受函数边界限制的内存。变量到底放哪由编译器做“逃逸分析”决定:如果变量的地址逃出了创建它的函数(被返回、存入堆、被闭包捕获等),就放堆上,否则留在栈上。
要点
- Go 的栈:每个 goroutine 初始约 2KB,可动态增长与收缩(连续栈,扩容时整段搬移并重写指针),栈上分配几乎零成本。
- Go 的堆:需要 GC 跟踪与回收,分配路径更重,堆对象越多 GC 压力越大。
- 常见逃逸诱因:返回局部变量指针、把指针存入 map/slice/interface、闭包捕获外部变量、变量过大栈放不下、接口装箱、fmt 打印指针等。
- 查看手段:go build -gcflags="-m" ./... 会输出每个变量的分配决策;配合基准测试对比更直观。
- 误区澄清:不是“用了指针就逃逸”,也不是“小对象一定在栈”;逃逸是编译期数据流分析的结论,与写法强相关。
- 工程意义:热路径减少不必要的堆分配(对象复用、避免临时接口装箱)可显著降低 GC 与延迟。
- 优化原则:先用 pprof/benchmark 定位“高频分配点”再动手,避免为逃逸而逃逸的过早优化。
示例
// 地址逃逸:返回局部变量指针,v 必须去堆上
func newVal() *int {
v := 42
return &v
}
// 接口装箱:形参是 any,实参常逃逸到堆
func printAny(x any) { fmt.Println(x) }
// 观察方法:go build -gcflags="-m" ./...
// 输出类似 "moved to heap: v",即逃逸决策点
常见追问/记忆点
- 追问:逃逸到堆一定不好吗?答:不一定,逃逸换来更灵活的生存期;只有高频分配才值得优化。
- 追问:怎么快速看分配?答:go test -benchmem、pprof 的 alloc_objects/alloc_space、-gcflags=-m。
- 追问:小对象为什么也可能在堆上?答:只要地址逃逸或编译器判定不划算,它就“晋升”到堆。
- 记忆:地址出去(被返回/入堆/进闭包)就得堆上;栈免费、堆要 GC 买单。