当向切片添加新参数时,底层数组会发生什么变化?它会扩展以容纳更多元素吗?
在这篇文章中,我们将深入探讨切片的内部工作原理,以及如何利用这些知识来进行更好的内存管理和性能优化。
具体而言,我们将探索 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()函数不仅仅是用于添加元素。它还负责处理切片的分配和调整大小。
这是我用更简单的方式重写的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)
}
下一次冒险
如果您对保持对软件工程领域的最新动态感兴趣,请关注我。我将确保让您了解最新信息!
请记住,始终保持学习的状态,并享受其中的乐趣,愉快的编程!
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved