Go语言中的切片(slice)是一种动态数组,那么第一个问题来了:
(1)动态数组
动态数组是一种在程序运行时可以根据需要自动调整其大小的数组。与传统的静态数组不同,动态数组不需要在编译时指定其大小,而是在运行时根据需要动态地分配和释放内存空间。 这种灵活性使得动态数组非常适合处理大小未知或大小可能变化的数据集合。
动态数组通常由一些高级编程语言的标准库提供,如C++中的std::vector
,Java中的ArrayList
,Python中的列表(list)等。这些实现通常提供了自动内存管理功能,包括在添加元素时自动扩展数组容量,以及在删除元素时可能进行的内存收缩(尽管一些实现可能为了效率而保留额外的空间)。
(2)静态数组
静态数组是在编译时确定大小,并在程序的生命周期内保持不变的数组。它们的大小在定义时就已经确定,并且在整个程序执行过程中都保持不变。如果尝试访问数组界限之外的元素,通常会导致未定义行为,比如程序崩溃。
静态数组通常存储在栈(stack)上,或者作为结构体或类的成员时,与结构体或类一起存储在堆(heap)上,其大小都是固定的。
(3)动态数组与静态数组的区别
Go slice包括三个关键的属性:指针、长度和容量。
Go语言的切片是引用类型,它们不存储任何数据,只描述底层数组的一段。更改切片的元素会修改其底层数组中的对应元素。
切片的长度和容量可以通过内置的 len() 和 cap() 函数获取,比如我们执行如下代码
func TestSlice(t *testing.T) {
var nums []int
for i := 0; i < 10; i++ {
nums = append(nums, i)
}
fmt.Println("nums = ", nums) // nums = [0 1 2 3 4 5 6 7 8 9]
fmt.Println("nums cap = ", cap(nums)) // nums cap = 16
fmt.Println("nums len = ", len(nums)) // nums len = 10
}
为什么10个元素的数组,容量是16?
因为Go slice的扩容机制:
Go语言的slice扩容策略在不同的版本和slice容量大小下有所不同,但总体思路是相似的,即创建一个更大的底层数组,并将原始数据复制到新数组中。
Go 1.7及之前版本,如果当前容量小于1024,每次扩容后的容量都会翻倍。如果当前容量大于等于1024,扩容后的容量会按照增长因子(默认为1.25)来计算,即新的容量为原容量的1.25倍。
Go 1.8及之后版本,如果当前容量小于256,每次扩容后的容量都会翻倍。如果当前容量大于等于256且小于4096,扩容后的容量会按照增长因子(这里为1.5)来计算。如果当前容量大于等于4096,扩容后的容量会按照增长因子(默认为1.25)来计算。
因为我用的Go版本是1.21,因此容量的走势就是 1、2、4、8、16。
Go 语言中 slice 的扩容机制主要通过 growslice
函数实现,该函数位于 Go 语言的 runtime/slice.go
文件中。由于源代码的具体实现可能会随着 Go 语言版本的更新而有所变化,这里将基于通用的理解和较新版本的 Go 语言(如 Go 1.18 及以后)来概述 slice 的扩容机制。
当 slice 的容量不足以容纳新添加的元素时,Go 语言会自动触发扩容机制。扩容的基本流程包括以下几个步骤:
1) 判断当前容量是否足够:首先检查当前 slice 的容量是否足够容纳新添加的元素。如果足够,则直接添加新元素;如果不足,则进行扩容。
2) 计算新的容量:按照上面说的规则。
3) 分配新的内存空间:根据计算出的新容量,分配足够的内存空间来存储新的 slice 底层数组。
4) 拷贝旧数据:将旧 slice 中的数据拷贝到新的内存空间中。
5) 更新 slice 信息:更新 slice 的长度和容量,并调整其底层数组指针指向新的内存空间。
由于直接引用 runtime/slice.go 文件中的 growslice
函数的完整源代码可能涉及大量的内部实现细节和版本特定的代码,这里仅提供该函数的一个大致框架和关键步骤的概述:
// 假设的伪代码,用于说明扩容机制
func growslice(et *_type, old slice, cap int) slice {
// ... 省略一些细节 ...
oldCap := old.cap // 旧的容量
newCap := oldCap // 初始时,新容量等于旧容量
// 计算新的容量
if oldCap < 256 {
newCap = oldCap * 2 // 容量小于 256 时,通常翻倍
} else {
// 容量大于等于 256 时,使用更平滑的增长因子
// 这里只是一个示例,实际实现可能更复杂
newCap = oldCap + oldCap/4 // 例如,增加 25%
// 或者使用更复杂的公式,如 newCap += (newCap + 3*threshold) / 4,其中 threshold 可能是 256 或其他值
// 确保新容量足够大
if newCap < cap {
newCap = cap
}
}
// ... 省略内存分配和拷贝旧数据的细节 ...
// 返回一个新的 slice,其底层数组指向新分配的内存空间
return slice{p: newPtr, len: old.len + numNewElems, cap: newCap}
}
Go 语言中 slice 的扩容机制之所以设计成这样,主要是基于以下几个方面的考虑:
1)性能优化:
2)内存使用效率
3)简化开发者负担
make
函数在创建 slice 时指定其初始容量,以更好地控制内存的使用和性能表现。4)平衡扩容与内存消耗
5)兼容性与未来优化
综上所述,Go 语言中 slice 的扩容机制是经过精心设计的,旨在通过合理的内存分配和扩容策略来优化程序的性能、内存使用效率和开发者体验。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。