slice本身不逃逸,但底层数组是否在堆上取决于make/append的参数和使用方式:容量n为运行时变量则必逃逸;含指针元素的切片几乎必然逃逸;返回slice不导致逃逸,暴露底层指针或跨goroutine共享才真正危险。

slice 本身不逃逸,但底层数组是否在堆上、何时被回收,完全取决于 make 或 append 的参数和使用方式 —— 不是看 slice 怎么返回,而是看它怎么被创建和修改。
make([]T, 0, n) 中 n 是变量就一定逃逸
Go 编译器对 make 的逃逸判定非常机械:只要容量参数 n 是运行时变量(比如函数入参、配置读取值),哪怕你只往里面 append 一个元素,make([]int, 0, n) 的底层数组也必定分配在堆上。
- 错误直觉:“我只用前几个元素,应该能栈上分配” → 编译器不看实际使用量,只看容量是否可静态推断
- 典型场景:
func handle(req *http.Request) { n := parseLimit(req); s := make([]byte, 0, n) }→s底层数组逃逸 - 验证方式:
go build -gcflags="-m -l"输出中出现make([]byte, 0, n) escapes to heap - 修复办法:若
n有合理上限,改用常量容量,如make([]byte, 0, 1024);或预估后切片重用(避免反复 make)
[]*T 类型切片几乎必然逃逸
哪怕容量是编译期常量,[]*string、[]*User 这类含指针元素的切片,其底层数组也几乎必然逃逸到堆。
- 原因:编译器认为“数组里存的是指针,这些指针可能被传出去”,为安全起见,整块底层数组直接上堆
- 常见误判:定义
var items []*User后只在函数内 append 并返回,仍看到逃逸日志 - 替代思路:改用
[]User(值类型),需要时再用&items[i]取地址;但要注意结构体大小,避免拷贝开销过大 - 注意嵌套:如
[]struct{ Name *string }同样逃逸,因为字段含指针
返回 slice 本身不导致底层数组逃逸
单纯写 return s(其中 s 是局部 slice)不会让底层数组逃逸。逃逸分析对象是 slice header(24 字节),不是底层数组。
- slice header 是值类型,复制开销小,接收方拿到的是副本,原 header 生命周期仍在当前栈帧
- 真正危险的是暴露底层控制权:比如
return s[:cap(s)]、return &s[0]、或把s传给接收interface{}的函数(如json.Marshal(s)、fmt.Printf("%v", s)) - 扩容影响:若函数内
s = append(s, x)触发了runtime.growslice,新底层数组会分配在堆上,但 header 仍可栈驻留;旧数组若无其他引用,会被 GC 回收 - 关键点:底层数组生命周期由所有持有其指针的 slice 共同决定 —— 即使 header 已出作用域,只要还有别的 slice 指向同一块内存,它就不会被回收
底层数组销毁周期容易被忽略的细节
底层数组的回收时机不取决于 slice header 的生命周期,而取决于是否还有任何活跃的 slice header 持有它的指针。
- 示例:
s1 := make([]int, 0, 100); s2 := s1; s1 = nil→s2仍持有原数组指针,数组不会被回收 - 更隐蔽的情况:把 slice 传入 goroutine、map、channel,或赋值给全局变量,都会延长数组生命周期
- GC 不保证立即回收:即使所有 slice 都已不可达,数组也可能在下一轮 GC 才被清理
- 最常被忽略的是:以为“函数返回了 slice 就等于把数组所有权交出去了”,其实只是交出了 header 副本,数组归属仍是共享的


















