什么是slice?
切片是go的一种数据类型,精确的说是基于数组延申的数据类型
切片的优点
-
切片可以动态增长或缩小
-
切片内部只包含指向底层数组的指针、切片的长度和容量。它避免了数组的内存拷贝,节省了内存开销。
-
切片提供了灵活的操作方式,如切片切割、创建子切片等。你可以很容易地创建一个新的切片,它指向底层数组的不同部分。
-
多个切片可以共享同一个底层数组。这种特性使得不同切片可以在不同上下文中高效地共享数据。
-
切片支持许多高级操作,如过滤、映射、排序等,利用内置函数和第三方库可以方便地进行这些操作(我的理解是数组可以用的第三方库如排序等切片也可用)。
切片的底层原理
切片的数据结构
Go 切片的底层结构通常用以下三部分表示:
- 指针(Pointer): 指向底层数组的起始位置。
- 长度(Length): 切片中实际包含的元素个数。
- 容量(Capacity): 从切片的起始位置到底层数组的末尾,包含的元素总数
type slice struct {
array unsafe.Pointer // 底层数组的指针
len int // 切片的长度
cap int // 切片的容量
}
下面的例子我们先创建一个数组arr当切片的底层数组 然后产生切片
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
// 创建一个数组
arr := [5]int{10, 20, 30, 40, 50}
// 创建一个切片,指向数组的一部分
slice := arr[1:4]//[1:4]截取的是1 2 3 下标的数组元素 所以切片的长度是3
// 使用 reflect.SliceHeader 获取切片的底层实现细节
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
fmt.Println("切片内容:", slice)
fmt.Println("底层数组地址:", sliceHeader.Data)
fmt.Println("切片长度:", sliceHeader.Len)
fmt.Println("切片容量:", sliceHeader.Cap)
}
运行结果:
为什么切片的长度是3但容量确实4呢
解释:[1:4]截取的是1 2 3 下标的数组元素 所以切片包含3个元素长度是3,
但容量是切片指针指向的地址到底层数组末尾的长度 即 4
在上面例子中就是 slice指向的arr[1]到arr尾端arr[4]的长度与
问题,切片修改元素后,底层数组对应的元素会被修改嘛?
当然会,因为切片说白了就是一种调用底层数组数据的数据结构
它只有指向底层数组得起始指针和长度容量,所有的操作都依附于底层数组
package main
import "fmt"
func main() {
// 创建一个底层数组和一个切片
arr := []int{1, 2, 3, 4, 5}
slice := arr[1:4] // 切片指向数组的部分
fmt.Println("Before modification")
fmt.Println("Slice:", slice) // 输出: [2 3 4]
fmt.Println("Array:", arr) // 输出: [1 2 3 4 5]
// 修改切片中的元素
slice[0] = 100
fmt.Println("After modification")
fmt.Println("Slice:", slice) // 输出: [100 3 4]
fmt.Println("Array:", arr) // 输出: [1 100 3 4 5] 底层数组也改变了
}
修改 slice[0]
直接修改了底层数组中索引 1
位置的值。
因为 slice
和 arr
共享同一个底层数组,所以 arr
中索引 1
位置的值也发生了变化。
面试问题:切片长度和容量的区别
切片长度(Length)
- 定义: 切片的长度表示切片中实际包含的元素个数。
- 作用: 你可以通过切片的长度来确定你可以安全地读取和操作切片中的多少个元素。
切片容量(Capacity)
- 定义: 切片的容量表示从切片的起始位置到底层数组的末尾之间可以容纳的元素总数。
- 作用: 切片的容量决定了切片在不重新分配底层数组的情况下,最多可以容纳多少个元素。即使切片的长度小于容量,你仍然可以向切片中追加元素,只要总长度不超过容量。
package main
import (
“fmt”
)
func main() {
// 创建一个长度和容量都为 5 的切片
slice := make([]int, 3, 5)
fmt.Println("初始切片内容:", slice)
fmt.Println("切片长度:", len(slice))
fmt.Println("切片容量:", cap(slice))
// 向切片中追加元素
slice = append(slice, 4, 5)
fmt.Println("追加元素后的切片内容:", slice)
fmt.Println("追加后的切片长度:", len(slice))
fmt.Println("追加后的切片容量:", cap(slice))
// 再次追加元素,超出当前容量
slice = append(slice, 6)
fmt.Println("再次追加元素后的切片内容:", slice)
fmt.Println("再次追加后的切片长度:", len(slice))
fmt.Println("再次追加后的切片容量:", cap(slice))
}
运行结果
问题:切片扩容机制 切片如何扩容的?切片扩容后会发生什么
在 Go 语言中,当对一个切片(slice)追加元素时,如果切片的容量已经满了,而扩容操作被触发,切片的容量会翻倍。具体来说,程序会执行以下步骤:
- 容量满了: 当切片的容量满了时,必须进行扩容以容纳更多的元素。
- 创建新底层数组: 程序会创建一个新的底层数组,其容量为原来数组的两倍。
- 复制数据: 原有底层数组中的数据会被复制到新的底层数组中。
- 更新切片: 切片的指针将会更新,指向新的底层数组。
因此:
- 未扩容时: 如果切片追加元素后没有触发扩容,切片的容量不会改变,切片指向的底层数组地址保持不变。
- 创建新底层数组: 程序会创建一个新的底层数组,其容量为原来数组的两倍。
- 复制数据: 原有底层数组中的数据会被复制到新的底层数组中。
- 更新切片: 切片的指针将会更新,指向新的底层数组。
因此:
- 未扩容时: 如果切片追加元素后没有触发扩容,切片的容量不会改变,切片指向的底层数组地址保持不变。
- 发生扩容时: 如果切片追加元素时发生了扩容,切片的容量会变成原来的两倍,切片指向的底层数组的地址会发生变化,因为底层数组已经被替换为一个新的、更大的数组。