Go 逃逸分析
2026-2-19 • 5min
什么是逃逸分析
逃逸分析(Escape Analysis) 是编译器在编译阶段执行的一种静态分析技术,它的核心目的是判断一个变量(或对象)会不会“逃逸”到堆上。
之所以要判断会不会逃逸到堆,主要是因为栈上分配内存非常快,函数返回就自动回收,不需要垃圾回收器介入。
堆上分配需要经过内存分配器 + 垃圾回收器管理,成本明显更高。
而编程语言之所以要区分堆还是栈,主要是因为栈大小必须在编译期(或函数开始时)确定,牺牲了灵活性。
由此我们也可以得出,一个对象会被分配到堆还是栈,由编译器在编译时决定。
决定的因素由很多,除了需要在编译期确定大小外,因为每个栈帧有大小限制,也不能存放太大的对象。
发生逃逸主要是以下 6 种场景:
| 场景 | 是否逃逸 | 原因 | |
|---|---|---|---|
| 1 | 对外部指针赋值 | 逃逸 | &x 被赋给了全局变量、结构体字段、闭包捕获、map/slice元素等 |
| 2 | 返回局部变量的指针 | 逃逸 | return &localVar |
| 3 | 局部变量被闭包捕获,且闭包逃逸 | 逃逸 | 闭包本身逃逸 → 捕获的变量也要跟着逃逸 |
| 4 | 动态大小的对象(如切片、map) | 几乎必逃 | 切片底层数组、map 本身通常都要逃逸(除非极小且完全可内联的情况) |
| 5 | interface{} 装箱 | 逃逸 | 任何值转成 interface{} 后几乎都会逃逸(因为接口里存的是指针) |
| 6 | 太大、无法确定大小的对象 | 逃逸 | 栈空间有限(通常几KB),编译器认为放栈上不安全时直接给堆 |
还有一点,容器类型的各个部分并不一定位于同一处,以切片为例,切片由三部分组成:指针 + 长度 + 容量,这三部分统称为 slice header,在 64 位系统上占用 24 个字节。slice header 和底层存储数据的数组,内存分配位置是分开考虑的。
| 部分 | 常见情况 | 能否栈分配? | 主要决定因素 |
|---|---|---|---|
| slice header | 局部变量 | 几乎总是可以 | 除非 header 本身逃逸(返回 &s、给 interface{} 等) |
| 底层数组(data) | make 时长度/容量已知且较小 | 可以栈分配 | 编译期知道确切大小 + 不逃逸 + 不太大 |
| 底层数组(data) | 长度/容量运行时决定 | 通常堆分配 | 大小未知,编译器无法预留栈空间 |
| 底层数组(data) | 长度 ≥ ~64KB(8192 个 int 左右) | 强制堆分配 | 即使不逃逸,栈太小(默认栈帧有限制) |
| 底层数组(data) | 发生 append 扩容 | 几乎必堆 | 可能需要重新分配,更难留在栈上 |
为什么需要逃逸分析
逃逸分析主要用于分析和优化内存分配,尤其是搞清楚:
- 哪些变量本来可以放在栈上,却被逃逸到了堆上?
- 为什么这个结构体/闭包/切片逃逸了?
- 能不能通过改写代码让更多变量留在栈上?
逃逸分析命令
go build -gcflags\="-m" .
命令的核心参数是:
- -gcflags:把后面的参数透传给底层编译器(go tool compile)
- “-m”:m = more escape analysis info
常见的用法变体:
# 最常用写法(推荐加 all= 显示所有包的逃逸信息)
go build -gcflags="all=-m" .
# 只看当前包
go build -gcflags="-m" .
# 只看不编译(最干净,只看逃逸分析,不生成可执行文件)
go build -gcflags="all=-m" -n .
输出:
./main.go:12:6: x escapes to heap
./main.go:15:17: []int literal escapes to heap