0%

Go 切片删除元素:底层数组真的“跳过”了一格吗?

Go 切片删除元素:底层数组真的“跳过”了一格吗?

在 Go 里删除切片中的某个元素,最常见的写法是这样一行:

1
2
3
nums := []int{10, 20, 30, 40, 50}
i := 2
nums = append(nums[:i], nums[i+1:]...) // 删除索引 2

运行后 nums 变成了 [10 20 40 50],30 不见了。

很多人第一次看到这行代码都会卡住:数组是一段连续的内存,怎么能“跳过”中间一格?

问一:底层数组真的跳过了一格吗?

没有,数组不能凭空少一格。实际上是后面的元素被逐个往前复制,把要删的那个覆盖掉了。

第 1 步:初始状态

第 2 步:切出两个窗口

nums[:2] 和 nums[3:] 都不是新数组,只是指向同一个底层数组的两个“窗口”:

注意 nums[:2] 的长度是 2,但容量是 5。

第 3 步:append 写入第 1 个元素,40 → 索引 2

原来的 30 被 40 直接覆盖掉了,并没有被跳过。

第 4 步:append 写入第 2 个元素,50 → 索引 3

后面的元素整体往前挪了一格。

第 5 步:返回新切片

append 返回一个长度为 4 的切片,所以你看到的是 [10 20 40 50]。

问二:为什么 append 不新建一个数组?

因为容量够用。nums[:2] 的长度是 2,容量是 5。append 要往里追加 2 个元素,2 + 2 = 4,没超过容量 5,所以它不分配新内存,直接从原数组的索引 2 开始写入。删除也就是在原数组上完成的。

问三:被删的 30 去哪了?末尾的 50 又是怎么回事?

30 被 40 覆盖,已经不存在了。底层数组始终是 5 格,最后一格还留着旧的 50,只是落在了新切片的范围之外,所以看不到。

可以写段代码验证:

1
2
3
4
5
6
7
nums := []int{10, 20, 30, 40, 50}
orig := nums // 记住完整的底层数组

nums = append(nums[:2], nums[3:]...)

fmt.Println(nums) // [10 20 40 50]
fmt.Println(orig) // [10 20 40 50 50] 原数组确实被改了

所以要注意:如果还有别的切片共用这个底层数组,它们会看到被改动后的数据。

问四:有更省心的写法吗?

Go 1.21 起,标准库提供了 slices.Delete:

1
2
3
import "slices"

nums = slices.Delete(nums, i, i+1)

效果和 append 写法相同,区别是它会把末尾残留的位置清成零值。如果切片里存的是指针,旧对象就不会因为残留的引用而无法回收。

一句话总结

删除不是跳过,是后面的元素整体前移一格,覆盖掉被删的那个,再把切片长度减一。

切片只是指向底层数组的窗口(指针、长度、容量),append 写入的是窗口背后的同一块内存。