Go 语言中同一 slice 上的切片其底层数组是否是同一个
第一部分:理解 Slice 和底层数组
什么是 Slice?
在 Go 语言中,slice 是一个轻量级的数据结构,用于管理一组具有相同类型的元素序列。Slice 提供了一种方便且灵活的方式来操作序列数据。
Slice 的结构
在 Go 中,一个 slice 包含三个部分:
- 指针(Pointer):指向底层数组的第一个元素。
- 长度(Length):表示 slice 中元素的个数。
- 容量(Capacity):底层数组中元素的总数,从 slice 的开始位置到底层数组的末尾。
package main import ( "fmt" ) func main() { // 创建一个底层数组 array := [5]int{1, 2, 3, 4, 5} // 使用切片来引用底层数组的一部分 slice := array[1:4] // 从索引为1的元素开始,到索引为3的元素结束,但不包括索引为4的元素 // 打印切片的指针、长度和容量 fmt.Printf("Pointer: %p, Length: %d, Capacity: %d\n", &slice[0], len(slice), cap(slice)) // 修改切片中的元素 slice[0] = 10 // 打印底层数组和切片中的元素 fmt.Println("Array:", array) fmt.Println("Slice:", slice) }
- 运行结果
切片的特性
- 切片是引用类型,即它们指向底层数组的某个连续片段。
- 切片可以动态增长,但不能动态缩减。
- 多个切片可以引用同一个底层数组。
package main import "fmt" func main() { // 创建一个底层数组 array := [5]int{1, 2, 3, 4, 5} fmt.Println("Capacity of array after appending:", cap(array)) // 输出:Capacity of array after appending: 5 // 创建切片1,引用数组的一部分 slice1 := array[1:4] fmt.Println("Slice 1:", slice1) // 输出:Slice 1: [2 3 4] // 修改切片1中的元素,同时底层数组也被修改 slice1[0] = 10 fmt.Println("Array after modifying slice 1:", array) // 输出:Array after modifying slice 1: [1 10 3 4 5] // 创建切片2,引用切片1的一部分 slice2 := slice1[1:3] fmt.Println("Slice 2:", slice2) // 输出:Slice 2: [3 4] // 修改切片2中的元素,同时切片1和底层数组也被修改 slice2[1] = 20 fmt.Println("Array after modifying slice 2:", array) // 输出:Array after modifying slice 2: [1 10 3 20 5] fmt.Println("Slice 1 after modifying slice 2:", slice1) // 输出:Slice 1 after modifying slice 2: [10 3 20] fmt.Println("Capacity of slice 1 after appending:", cap(slice1)) // 输出:Capacity of slice 1 after appending: 4 // 动态增长切片 向切片 slice1 中添加元素 6 和 7 slice1 = append(slice1, 6, 7) fmt.Println("Slice 1 after appending:", slice1) // 输出:Slice 1 after appending: [10 3 20 6 7] fmt.Println("Array after appending to slice 1:", array) // 输出:Array after appending to slice 1: [1 10 3 20 6] // 注意:切片的容量不变,仍然指向原底层数组的部分 fmt.Println("Capacity of slice 1 after appending:", cap(slice1)) // 输出:Capacity of slice 1 after appending: 8 fmt.Println("Capacity of array after appending:", cap(array)) // 输出:Capacity of array after appending: 5 }
- 运行结果
这个运行结果说明了切片和底层数组的特性,以及切片操作的影响:
- 底层数组的容量:
- 初始时,数组 array 的容量为 5。
- 切片1的创建和修改:
- 创建了切片1 slice1,它引用了数组的一部分,即 [2 3 4]。
- 修改了切片1 slice1 中的第一个元素为 10,底层数组也相应地被修改。
- 切片2的创建和修改:
- 创建了切片2 slice2,它引用了切片1的一部分,即 [3 4]。
- 修改了切片2 slice2 中的第二个元素为 20,底层数组和切片1也相应地被修改。
- 切片1的动态增长:
- 使用 append() 向切片1 slice1 中添加了元素 6 和 7。
- 输出了切片1 slice1 的容量,发现容量在追加元素后变为了 8。
- 底层数组和切片容量的关系:
- 切片的容量可以随着追加元素的增加而增加,但底层数组的容量保持不变。这就是为什么在追加元素后,切片1的容量增加到了8,而底层数组的容量仍然保持在5的原因。
底层数组是否相同的判断
当我们在同一 slice 上创建不同的切片时,它们是否共享同一个底层数组呢?我们来看看以下示例代码:
package main import "fmt" func main() { // 创建一个切片 slice1 slice1 := []int{1, 2, 3, 4, 5} // 创建 slice1 的两个切片:slice2 和 slice3 slice2 := slice1[1:3] slice3 := slice1[2:5] // 修改 slice2 的元素值 slice2[0] = 10 // 输出 slice1、slice2 和 slice3 的内容 fmt.Println("slice1:", slice1) // 输出:slice1: [1 10 3 4 5] fmt.Println("slice2:", slice2) // 输出:slice2: [10 3] fmt.Println("slice3:", slice3) // 输出:slice3: [3 4 5] }
- 运行结果
你的代码创建了一个切片 slice1,然后又基于 slice1 创建了两个新的切片 slice2 和 slice3。接着,修改了 slice2 的第一个元素的值为 10。最后,输出了三个切片的内容。
这个结果说明了切片的特性:
- 切片是对底层数组的引用,因此对切片的修改会影响底层数组以及其他引用相同底层数组的切片。
- 创建 slice2 和 slice3 时,它们指向的是 slice1 中相应索引范围的元素。所以 slice2 中的修改影响了 slice1,而 slice3 没有受到影响。
切片的应用场景
切片是 Go 中非常常用的数据结构,它们在很多场景下都能发挥重要作用,例如:
- 动态数组:切片提供了比数组更灵活的动态增长功能,因此在需要动态管理数据集合时非常有用。
- 函数参数和返回值:使用切片作为函数参数和返回值,可以避免拷贝大量数据,提高程序的性能。
- 数据过滤和操作:通过切片,可以方便地对数据进行过滤、排序和操作,提高代码的可读性和可维护性。
下面是一个演示切片应用场景的示例代码:
package main import "fmt" // 动态数组 func dynamicArray() { // 声明一个切片 var numbers []int // 使用 append() 函数动态增加元素 numbers = append(numbers, 1) numbers = append(numbers, 2, 3, 4) fmt.Println("Dynamic Array:", numbers) } // 函数参数和返回值 func processSlice(input []int) []int { // 对切片进行操作 for i := range input { input[i] *= 2 } return input } // 数据过滤和操作 func filterSlice(input []int) []int { var filtered []int for _, num := range input { if num%2 == 0 { filtered = append(filtered, num) } } return filtered } func main() { // 动态数组 dynamicArray() // 函数参数和返回值 original := []int{1, 2, 3, 4, 5} processed := processSlice(original) fmt.Println("Processed Slice:", processed) // 数据过滤和操作 data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} filtered := filterSlice(data) fmt.Println("Filtered Slice:", filtered) }
运行结果:
这个示例展示了切片在不同场景下的应用:
- 动态数组:使用 append() 函数动态地向切片中添加元素,实现了动态数组的功能。
- 函数参数和返回值:processSlice() 函数接受一个切片作为参数,对其进行操作后返回。这种方式避免了对大量数据进行拷贝,提高了程序性能。
- 数据过滤和操作:filterSlice() 函数对切片中的元素进行过滤,仅保留偶数。这样可以方便地对数据进行操作,提高了代码的可读性和可维护性。