底层数组不会立即释放,只要存在任何引用(包括子切片、unsafe.Pointer等),GC就不会回收;Go 1.22+在唯一引用时可in-place扩容复用数组;赋nil不能确保释放,需主动切断所有引用。
不会立即释放,甚至可能根本不会释放——只要还有任何变量持有对原底层数组的引用,gc 就不会回收它。
扩容时底层数组是否复用取决于引用计数
Go 运行时在 growslice 中判断是否新建数组,关键不是“扩不扩容”,而是“该底层数组是否还有其他 slice 在用”:
- 如果原 slice 是唯一持有者(引用计数为 1),Go 1.22+ 可能直接 in-place 扩容:复用原数组、仅调整
cap,不分配新内存,自然也谈不上“释放” - 如果有子切片(如
s2 := s[2:4])或别名变量仍存活,即使你把原 slice 设为nil,底层数组依然被强引用,GC 不会动它 - 只有所有引用(包括通过
unsafe.Pointer(&s[0])间接持有的)全部消失,引用计数归零,下次 GC 才可能回收
常见误判场景:以为赋值 nil 就安全了
很多人写 s = nil 后就认为底层数组可回收,但实际常踩坑:
-
s1 := make([]int, 0, 100)→s2 := s1[10:20]→s1 = nil:此时s2仍指向原数组第 10 个元素起始位置,整个底层数组继续存活 - 函数返回一个子切片(如
return data[100:150]),调用方拿到后,原始大 slice 即使被丢弃,底层数组也不会释放 - 用
fmt.Printf("%p", &s[0])看地址没变,不代表没风险——in-place 扩容后地址不变,但若后续又 append 导致二次扩容,旧数组才真正“被抛弃”,而那时是否回收仍看引用
如何确认并主动切断引用
不能靠猜,得用明确手段隔离或释放:
- 验证地址变化:
oldPtr := unsafe.Pointer(&s[0])→s = append(s, x)→newPtr := unsafe.Pointer(&s[0]);若oldPtr != newPtr,说明已启用新数组,但旧数组是否回收仍需检查其余引用 - 显式截断再丢弃:
s = s[:0]清空长度(不释放底层数组),再让变量自然出作用域或设为nil;但这只切断当前 slice 的引用,不影响子切片 - 强制复制隔离:
newS := make([]int, len(s)); copy(newS, s),之后原s和newS各自持有独立底层数组,原s可放心丢弃
最易被忽略的一点:子切片的生命期往往比父 slice 长得多,尤其在返回值、闭包捕获、结构体字段中。只要有一个地方还拿着 s[i:j] 的结果,整块底层数组就得一直留着——哪怕你只用了其中两个元素。











