当向切片添加新参数时,底层数组会发生什么变化?它会扩展以容纳更多元素吗?
在这篇文章中,我们将深入探讨切片的内部工作原理,以及如何利用这些知识来进行更好的内存管理和性能优化。
具体而言,我们将探索 Go 中切片的底层实现和内存管理机制。
让我们开始吧!
查看数组 要深入了解切片的结构,必须仔细查看其底层类型:数组。
func main () { a := [5 ]int {} fmt.Printf("%p, %p\n" , &a, &a[0 ]) }// 0x14000018240, 0x14000018240 正如您可能已经了解的那样,数组中第一个元素的内存位置也是数组本身的内存位置(这意味着当您将数组传递给函数或赋值给变量时,您实际上是传递或赋值了第一个元素的内存地址)。
因此,数组的内存布局是连续的内存块,每个元素依次放置在相邻的位置上。
切片的结构 切片有三个主要组成部分:
这些关于切片结构的信息是从Go运行时库中获取的。现在,让我们更详细地了解一下。
type slice struct { array unsafe.Pointer len int cap int }出于演示目的,这里有一个关于切片长度和容量概念的示例(如果你已经熟悉这些概念,可以忽略这个示例)。
func main () { original := []int {0 , 1 , 2 , 3 , 4 } s := original[1 :2 ] fmt.Println(len (s), cap (s)) }// 1 4 在这个示例中,我们可以看到切片s等于[]int{1},它的容量是从原始数组的索引1到索引4的部分计算得到的。
底层数组将会改变 需要注意的是,修改切片中的元素有时会影响到底层的数组,但并非总是如此,也不应该依赖这种行为。
在某些情况下,底层的数组可能会发生改变,导致切片也发生改变。然而,在编写代码时不应该依赖这种行为,因为它可能会导致意想不到的结果。
func main () { original := []int {0 , 1 , 2 , 3 , 4 } s := original[:] fmt.Println("Same array:" ) s[0 ] = 100 fmt.Println(original, s) fmt.Println("Different array:" ) s = append (s, 5 ) s[0 ] = 200 fmt.Println(original, s) }// Same array: // [100 1 2 3 4] [100 1 2 3 4] // Different array: // [100 1 2 3 4] [200 1 2 3 4 5] 在实际情况中,append()函数不仅仅是用于添加元素。它还负责处理切片的分配和调整大小。
1. 它会检查切片是否有足够的容量来存储新的元素。
2. 如果容量不足,它会创建一个具有更大容量的新切片,复制 原始切片的元素到新切片,并将新切片赋值给原始切片。
3. 然后,它将新的元素添加到切片中。
这是我用更简单的方式重写的append()函数版本,利用了泛型:
func append [T any ](s []T, x ...T) []T { n := len (s) maxN := len (s) + len (x) // If there is not enough capacity, create a new slice with larger capacity if n+len (x) > cap (s) { newSlice := make ([]T, maxN, maxN*2 ) copy (newSlice, s) // Copy the elements from the original slice to the new slice s = newSlice } s = s[:maxN] copy (s[n:], x) return s }预分配技术 重新调整切片大小在性能和内存方面可能非常昂贵 ,因为它需要分配一个新的切片并将所有元素复制过去。
这就是为什么在使用切片时,如果我们可以预测它们将保存的元素数量,通常最好进行预分配。这有助于提高性能并防止不必要的内存分配。
“是否可以同时使用“append()”和预分配?使用索引赋值可能很麻烦”
是的,这是可能的。
你可以使用make()函数进行预分配切片,传入两个变量,一个用于长度,另一个用于容量,而不是只传入一个。这可以消除索引赋值的需要。
func main() { s := make([]int, 0, 3) s = append2(s, 1, 2, 3, 4) fmt.Println(s) }下一次冒险