Golang中为什么切片缩容到零长度不会释放元素?
Golang中为什么切片缩容到零长度不会释放元素? 大家好
我正在回顾一些代码以复习知识,并在 Go 之旅中发现了数组示例,但我不理解为什么通过设置 array = array[:0] 来减小大小并不会丢弃元素。
以下代码输出:
len=5 cap=5 [1 2 3 4 5] // 原始数组 len=0 cap=5 [] // 数组被缩减 len=5 cap=5 [1 2 3 4 5] // 现在我们可以回到原始数组(元素仍然存在) len=3 cap=3 [3 4 5] // 如果我们使用 [N:],元素会从数组中丢弃
那么,为什么 s[:0] 不会丢弃元素,并且与 s[N:] 的行为不同?
这是设计使然吗?
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
printSlice(s)
// 将切片切片为零长度。
// 元素不会被丢弃
s = s[:0]
printSlice(s)
// 将数组扩展回原始大小,
// 所有元素仍然存在
s = s[:5]
printSlice(s)
// 丢弃其前两个值。
// 但为什么它会丢弃元素,而我们无法回到原始状态?
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
更多关于Golang中为什么切片缩容到零长度不会释放元素?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中为什么切片缩容到零长度不会释放元素?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在 Go 博客中有一篇讨论此问题的文章。
Go 切片:用法与内部实现 (Go Slices: usage and internals - The Go Programming Language)
搜索“一个可能的‘陷阱’”部分,其中提到: 如前所述,对切片进行重新切片并不会复制底层数组。整个数组将一直保留在内存中,直到不再被引用。有时,这可能导致程序在只需要一小部分数据时,却将全部数据都保留在内存中。
我尝试通过先将切片赋值为 nil 来解决这个问题,然后重新进行修改。
s = nil
printSlice(s)
// s = s[:3]
// panic: runtime error: slice bounds out of range [:3] with capacity 0
s = []int{9, 8, 7, 6, 5}
printSlice(s) // 不再有泄漏
s = s[:5]
printSlice(s)
这样做对吗?
我会研究一下这个问题。那么底层数组仍然在内存中,我们只是将指针向前移动了?
我原本以为我们得到了一个新的内存数组,因为输出中的内存地址发生了变化:
原始地址是
address=0xc00000e1e0
使用下限切片 s[2:] 后,我们得到了一个不同的地址:
address=0xc00000e1f0
%p 打印的地址代表的是切片的内存地址,而不是底层数组的内存地址吗?
切片后得到的这个不同地址,是否代表了内存中一个新的切片分配?
嘿,我仔细研究了那些关于 Split 函数分割数组及其结果切片的例子,现在我能看到一些之前没注意到的东西了。有时,切片可能指向底层数组的相同内存地址,有时则可能指向一个全新的内存地址,这取决于某些特定条件。我想,如果我们不了解返回切片的函数行为,切片可能会带来很多麻烦。或许我们可以尝试通过创建测试来确认预期行为,从而避免切片相关的问题。谢谢克里斯托夫和各位。 
是的,通过你的例子更容易看清地址发生了什么:
Go 是一种开源编程语言,它使得构建简单、可靠且高效的软件变得容易。
slicing with high limit
arr add=0xc0000b8000
sl add=0xc0000b8000
&arr[0] == &sl[0]: true
len(arr) == cap(sl): true
slicing with low limit
arr add=0xc0000b8000
sl add=0xc0000b8002 <--- 位置增加了
*** &arr[0] == &sl[0]: false ***
*** &arr[2] == &sl[0]: true ***
当你从原始数组(或切片)的前端进行切片时,生成的切片只是将其指针递增到你所切到的元素的指针。
在你的示例中,原始地址 0xc00000e1e0 与 s[2:] 的起始地址 0xc00000e1f0 之间的差值是 16 字节。
原始切片中的第一个元素(索引 0)是 0xc00000e1e0。
原始切片中的第二个元素(索引 1)是 0xc00000e1e8(0xc00000e1e0 + 8 字节)。
原始切片中的第三个元素(索引 2)是 0xc00000e1f0(0xc00000e1e0 + 16 字节)。
这就是为什么 s[2:] 得到的是 0xc00000e1f0。
对切片进行重新切片永远不会导致新的内存分配。
通过将下限设置为2来跳过前两个值时,会在内存中创建一个具有不同长度和容量的新数组。
这是不正确的。对切片进行重新切片永远不会导致新的内存分配。我试图在上一个帖子中用“ASCII艺术”来描绘这一点,但我想可能表达得不够清楚。
一个切片包含3个字段:
- 指向切片中第一个元素的指针
- 切片的长度
- 切片的容量
如果我有一个内存数组:var arr [10]byte
然后我可以从中创建一个切片:var sl = arr[:]
之后,以下等式成立:
&arr[0] == &sl[0]
len(arr) == len(sl)
len(arr) == cap(sl)
正如您所注意到的,当您从末尾开始切片时,减少的是长度。指向第一个元素的指针和容量保持不变。
然而,当您从切片的开头开始切片时,发生的情况是指向第一个元素的指针被更改为指向您开始切片的第n个元素(即,sl = arr[2:] 意味着将指针设置为指向 &arr[2])。因为切片的头部向前移动了2个位置,在这种情况下,长度和容量必须减少2。
如果您写入 sl[0],结果将出现在 arr[2] 中。
这里有一个可运行的示例:Go Playground - The Go Programming Language
Go 切片由三个字段组成:
- 指向切片中第一个元素的指针
- 切片的长度
- 切片底层内存的容量
当你将切片重新切片为更小的长度时,指向第一个元素的指针和容量保持不变,但长度会减少(在 slice[:0] 的情况下,长度减少到 0,但其他两个字段保持不变)。
当你从切片的前端重新切片时,会发生一些不同的事情:指向第一个元素的指针被设置为你正在切片的第 n 个元素的指针(即 slice[2:] 意味着将指针从指向 &slice[0] 改为指向 &slice[2])。然后,在这种情况下,长度和容量都减少 2:
memory:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
s:
+-+-+-+-+-+
| | | | | |
+-+-+-+-+-+
^ ^
| |
+- data |
len: 5-+
cap: 5-+
s[:0]
+-+-+-+-+-+
| | | | | |
+-+-+-+-+-+
^ ^
| |
+- data |
+- len: 0 |
cap: 5-+
s[2:]
+-+-+-+
| | | |
+-+-+-+
^ ^
| +-+
+- data |
len: 3
cap: 3
虽然在技术上可以使用 unsafe 和 reflect 包来操作切片数据,以将切片从 s[2:] "倒回"到 s[0:],但在一般情况下,你的切片可能来自任何地方,这样做是危险的,因为你不知道传入你函数的切片是来自 s[2:] 还是普通的 s。
我不太理解你在这个上下文中提到的“泄漏”是什么意思。
在我的测试中,我发现当我们像 s[2:] 这样使用下限来切片一个已填充的数组时,我认为我们会得到一个全新的底层数组,它位于一个新的内存地址中。因此我们无法“回退”(倒回),因为位置 0 和 1 上没有元素。
package main
import "fmt"
func main() {
// 填充数组
s := []int{1, 2, 3}
printSlice("s | ", s)
// 将数组切片使其长度为零。
// 元素并未被丢弃。底层数组的内存地址仍然相同
s = s[:0]
printSlice("s[:0] | ", s)
// 将数组扩展回原始大小,
// 所有元素仍然存在于底层数组的同一内存地址中
s = s[:3]
printSlice("s[:3] | ", s)
// 当通过将下限设置为 2 来跳过前两个值时,
// 内存中会创建一个具有不同长度和容量的新数组
s = s[2:] // 底层数组被替换为一个新地址
printSlice("s[2:] | ", s)
}
func printSlice(m string, s []int) {
fmt.Printf("%v len=%d cap=%d address=%p value=%v\n", m, len(s), cap(s), s, s)
}
输出
s | len=3 cap=3 address=0xc00000e1e0 value=[1 2 3] // 相同内存地址
s[:0] | len=0 cap=3 address=0xc00000e1e0 value=[] // 相同内存地址
s[:3] | len=3 cap=3 address=0xc00000e1e0 value=[1 2 3] // 相同内存地址
s[2:] | len=1 cap=1 address=0xc00000e1f0 value=[3] // 哎呀,我们在一个新的内存地址得到了数组!
如果我错了请纠正我,但我猜当我们调用来自其他包的方法来切片我们的数组时,我们必须了解实现细节,因为这些方法如果使用了下限,可能会改变数组并生成一个具有不同长度和容量的新地址。
在我看来,对于刚开始学习 Go 的人来说,切片的内部机制有点难以理解。
在Go语言中,切片操作 s[:0] 不会释放底层数组元素,而 s[N:] 会丢弃前N个元素,这是由切片的内存模型和设计决定的。
切片包含三个部分:指向底层数组的指针、长度(len)和容量(cap)。s[:0] 只是将长度设置为0,但底层数组指针和容量保持不变,因此元素仍然保留在底层数组中。而 s[N:] 会移动指针位置,丢弃前面的元素,因为新的切片指向底层数组的中间位置。
示例代码说明:
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Printf("原始切片: len=%d cap=%d %v\n", len(s), cap(s), s)
fmt.Printf("底层数组地址: %p\n", &s[0])
// s[:0] 操作
s1 := s[:0]
fmt.Printf("s[:0] 后: len=%d cap=%d %v\n", len(s1), cap(s1), s1)
fmt.Printf("底层数组地址不变: %p\n", &s[0])
// 仍然可以通过完整切片访问元素
fmt.Printf("完整底层数组: %v\n", s[:cap(s)])
// s[2:] 操作
s2 := s[2:]
fmt.Printf("s[2:] 后: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
fmt.Printf("底层数组指针移动: %p\n", &s2[0])
// 无法再访问前两个元素
}
输出:
原始切片: len=5 cap=5 [1 2 3 4 5]
底层数组地址: 0xc00001a0f0
s[:0] 后: len=0 cap=5 []
底层数组地址不变: 0xc00001a0f0
完整底层数组: [1 2 3 4 5]
s[2:] 后: len=3 cap=3 [3 4 5]
底层数组指针移动: 0xc00001a100
关键区别:
s[:0]保持指针不变,仅修改长度s[N:]移动指针位置,丢弃前面的元素
这是Go切片设计的特性,s[:0] 常用于清空切片而保留容量,s[N:] 用于截断头部元素。

