Go语言(Golang)中切片(slice)的相关知识、包括切片与数组的关系、底层结构、扩容机制、以及切片在函数传递、截取、增删元素、拷贝等操作中的特性。并给出了相关代码示例和一道面试题。关键要点包括:
数组特性:Go语言中数组是一个值、数组变量表示整个数组、不同于C语言中指向第一个元素的指针。传递数组到函数或拷贝数组时、会有不同的内存地址和数据独立性表现。
切片定义:切片是建立在Go数组之上的抽象类型、其底层结构包含指向底层数组的指针、长度和容量。
切片扩容:
- 新切片长度大于旧切片容量两倍时、新容量为新长度
- 旧容量小于256时、新容量为旧容量两倍
- 否则按1.25倍增速扩容、还会进行内存对齐。
函数传递:切片通过函数传递时、传的是切片结构、在函数内改变切片可能影响函数外的切片、取决于底层数组是否变化。
切片操作:
- 通过 作截取切片、新切片与原切片共享底层数组
- 删除元素可通过拼接切片实现
- 新增元素使用append操作
- 深度拷贝可使用copy函数。
1.切片是什么
在Go语言中 切片(slice)是建立在数组之上的一种抽象类型。切片提供了一种更灵活的方式来处理数组、它允许动态地改变数组的大小、并且可以方便地进行切片操作。理解切片之前、我们需要先了解数组。
Go的数组 在Go语言中、数组的长度是类型的一部分、这意味着数组的长度是固定的、不能改变。
数组的传递和拷贝行为与C语言不同、Go语言中的数组是值类型、传递数组时会进行值拷贝。
1.1 示例一:
将数组传递到函数中 数组的地址不一样- package main
- import "fmt"
- func main() {
- array := [3]int{1, 2, 3}
- // 数组传递到函数中
- test(array)
- fmt.Printf("array 外: %p\n", &array)
- }
- func test(array [3]int) {
- fmt.Printf("array 内: %p\n", &array)
- }
复制代码 由于数组是值类型、传递数组时会进行值拷贝、因此在函数中打印的地址与函数中打印的地址不同。
1.2 值拷贝
值拷贝意味着拷贝的是变量的内容、而不是内存地址。因此拷贝出来的变量有自己的独立副本、内容相同、但它们存储在不同的内存地址中。
Go 语言中的切片(slice)是动态扩容的。当你向切片中添加元素时、Go 会自动管理切片的大小、并在需要时进行扩容。
具体行为:
- 初始容量:当你创建一个切片时、Go 会为切片分配一个初始容量。如果你添加的元素超过了切片当前的容量Go 会自动扩容。
- 扩容规则:Go 会根据当前切片的容量自动扩展切片的大小、通常是原来容量的2倍。扩容后、切片的长度和容量都会增加。
- 内部机制:当切片扩容时、Go 会为新切片分配新的底层数组、并将原数组的元素拷贝到新数组中。这是一个代价比较高的操作、尤其是在需要多次扩容的情况下
2.底层结构
- type slice struct {
- // 底层数组指针(或者说是指向一块连续内存空间的起点)
- array unsafe.Pointer
- // 长度
- len int
- // 容量
- cap int
- }
复制代码 在这个结构中:
- :指向底层数组的指针、或者说是指向一块连续内存空间的起点。
- :切片的长度、即切片中实际包含的元素数量。
- :切片的容量、即切片可以包含的元素的最大数量,不包括可能的扩展空间。
切片扩容
计算目标容量如果新切片的长度大于旧切片容量的两倍、则新切片容量就为新切片的长度。
- 如果旧切片的容量小于256、那么新切片的容量就是旧切片的容量的两倍。
- 反之需要用旧切片容量按照1.25倍的增速、直到大于新切片长度。
为了更平滑的过渡、每次扩大1.25倍、还会加上进行内存对齐、需要按照Go内存管理的级别去对齐内存、最终容量以这个为准。
3.切片问题
3.1 切片通过函数传的是什么
- package main
- import (
- "fmt"
- "reflect"
- "unsafe"
- )
- func main() {
- s := make([]int, 5, 10)
- PrintSliceStruct(&s)
- test(s)
- }
- func test(s []int) {
- PrintSliceStruct(&s)
- }
- func PrintSliceStruct(s *[]int) {
- // 代码 将slice 转换成 reflect.SliceHeader
- ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
- // 查看slice的结构
- fmt.Printf("slice struct: %+v, slice is %v\n", ss, s)
- }
复制代码 控制台输出:- slice struct: &{Data:1374389649568 Len:5 Cap:10}, slice is &[0 0 0 0 0]slice struct: &{Data:1374389649568 Len:5 Cap:10}, slice is &[0 0 0 0 0]
复制代码 切片的定义:你创建了一个切片 s、通过 make([]int, 5, 10) 创建了一个长度为 5、容量为 10 的切片。
也就是说它初始化了一个包含 5 个元素且最大容量为 10 的底层数组
总结:
切片传递:当切片通过参数传递到函数时、传递的是切片的值、但切片内部的底层数组地址(指针)并没有被复制。 打印的结构:无论是在 main 函数还是 test 函数中、切片的底层数组地址、长度和容量都是相同的、因为底层数组是共享的。
为什么输出相同:
输出显示的 Data 地址、Len 和 Cap 是一致的、因为 test(s) 传递的是切片的值(即切片的结构),但切片中的指针指向相同的底层数组。所以无论是传递给函数的 s、还是 test 函数中的 s、它们指向的是同一个底层数组、并且它们的长度和容量保持一致
3.2 在函数里面改变切片 函数外的切片会被影响吗
- package main
- import (
- "fmt"
- "reflect"
- "unsafe"
- )
- func main() {
- s := make([]int, 5) // 创建一个长度为 5 的切片
- case1(s) // 调用 case1 函数
- case2(s) // 调用 case2 函数
- PrintSliceStruct(&s) // 打印切片结构
- }
- // 底层数组不变
- func case1(s []int) {
- s[1] = 1 // 修改切片中的元素
- PrintSliceStruct(&s) // 打印切片结构
- }
- // 底层数组变化
- func case2(s []int) {
- s = append(s, 0) // 扩容切片
- s[1] = 1 // 修改切片中的元素
- PrintSliceStruct(&s) // 打印切片结构
- }
- func PrintSliceStruct(s *[]int) {
- // 将切片转换成 reflect.SliceHeader
- ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
- // 打印切片的底层结构
- fmt.Printf("slice struct: %+v, slice is %v\n", ss, *s)
- }
复制代码 关键点:函数:
在中、你传入一个长度为 5 的切片 s、并修改切片中的元素。
切片在函数内的操作是对原切片的修改、因此底层数组没有发生变化、切片的容量、长度仍然相同。
打印的的 Data、和字段显示的是切片的原始底层数据结构。函数:
在中、你向切片添加一个元素(通过 append 操作)、这将可能导致切片的底层数组扩容。
因为操作在超出当前容量时会触发扩容、所以 s 的底层数组会发生变化、容量也可能增加。
在中、s 被赋值为 append(s, 0)、这将导致原有切片 s 的底层数组被扩展、并且一个新的数组被分配给 s(s 指向的是新的底层数组)
打印时会看到中的 Data 指向一个新的地址、表示底层数组已经发生了变化。函数会检查切片是否有足够的容量来存储新的元素。如果切片的容量不足、函数会分配一个新的更大的数组、并复制旧数组的内容到新数组中、然后将新元素添加到新数组的末尾、并更新切片的指针以指向包含新元素的新底层数组。
3.3 截取切片
- package main
- import (
- "fmt"
- "reflect"
- "unsafe"
- )
- func main() {
- s := make([]int, 5) // 创建一个长度为 5 的切片,默认初始化为 [0 0 0 0 0]
-
- case1(s) // 调用 case1,修改切片内容
- case2(s) // 调用 case2,修改切片并改变底层数组
- case3(s) // 调用 case3,截取切片并改变其长度
- case4(s) // 调用 case4,截取切片的部分元素
- PrintSliceStruct(&s) // 最后打印切片的底层结构
- }
- // case1:修改切片元素、底层数组不变
- func case1(s []int) {
- s[1] = 1 // 修改切片中的第二个元素,s[1] = 1
- PrintSliceStruct(&s) // 打印修改后的切片底层结构
- }
- // case2:重新赋值为新的切片
- func case2(s []int) {
- s = s[:] // 这里实际上并没有改变切片的内容、它只是重新赋值为原切片的一个新引用。
- PrintSliceStruct(&s) // 打印新的切片底层结构
- }
- // case3:截取切片、底层数组不变
- func case3(s []int) {
- s = s[:len(s)-1] // 截取切片、去掉最后一个元素、新的切片长度为 4
- PrintSliceStruct(&s) // 打印截取后的切片底层结构
- }
- // case4:截取切片的部分元素、底层数组不变
- func case4(s []int) {
- sl := s[1:2] // 截取 s[1:2],即取出切片中索引为 1 的元素
- PrintSliceStruct(&sl) // 打印截取后的新切片底层结构
- }
- // PrintSliceStruct 打印切片的底层结构
- func PrintSliceStruct(s *[]int) {
- // 将切片的指针转换为 reflect.SliceHeader 结构体,通过 unsafe.Pointer 获取底层数据
- ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
-
- // 打印切片的底层数据结构、包括:指向底层数组的内存地址、切片的长度和容量
- fmt.Printf("slice struct: %+v, slice is %v\n", ss, *s)
- }
复制代码 总结:
切片操作的影响:
修改切片元素不会改变底层数组的地址。
重新赋值切片并没有改变底层数组、除非涉及扩容(例如 append)。
截取切片时、底层数组不变、切片的长度和容量可能会变化。
3.4 删除元素
- package main
- import (
- "fmt"
- "reflect"
- "unsafe"
- )
- func main() {
- // 创建一个包含5个整数的切片
- s := []int{0, 1, 2, 3, 4}
- // 打印切片的底层结构
- PrintSliceStruct(&s)
- // 删除切片中的最后一个元素,正确的做法是通过切片截取
- _ = s[4] // 访问并丢弃切片中的最后一个元素
- s1 := append(s[:1], s[2:]...) // 删除元素 s[1],
- //s[:1](即切片 [0])和 s[2:](即切片 [2, 3, 4])拼接在一起。
- // 打印修改后的切片
- fmt.Println(s) // [0 2 3 4 4]
- fmt.Println(s1) // [0, 2, 3, 4]
- // 打印切片底层结构
- PrintSliceStruct(&s)
- PrintSliceStruct(&s1)
- // 访问切片的元素
- s = s[:4] // 截取切片、删除最后一个元素
- _ = s[3] // 访问切片中的最后一个元素(索引为3的元素)
- }
- // 打印切片的底层结构
- func PrintSliceStruct(s *[]int) {
- // 将切片转换为 reflect.SliceHeader
- ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
- // 打印切片的底层结构
- fmt.Printf("slice struct: %+v, slice is %v\n", ss, *s)
- }
复制代码 在 Go 中、切片操作需要特别注意切片的索引和截取。访问切片中的元素时要小心类型不匹配(例如不能将一个切片元素赋值给切片)。
控制台输出:- slice struct: &{Data:1374390755328 Len:5 Cap:5}, slice is [0 1 2 3 4][0 2 3 4 4][0 2 3 4]slice struct: &{Data:1374390755328 Len:5 Cap:5}, slice is [0 2 3 4 4]slice struct: &{Data:1374390755328 Len:4 Cap:5}, slice is [0 2 3 4]
复制代码 打印原切片 s 时、它仍然指向原底层数组(长度为 5、容量为 5)、而且由于 s[4] 在内存中并没有被移除、原底层数组中的最后一个元素 4 被保留、因此 s 显示为 [0 2 3 4 4]。
简而言之s 显示为 [0 2 3 4, 4] 是因为原始切片的底层数组并没有被修改、而操作生成了一个新的切片(s1)并分配了新的底层数组。所以s 中仍然包含原数组中的所有元素、最后一个 4 仍然存在。
为什么 s变成了 [0, 2, 3, 4, 4]
append 会根据切片的容量决定是否会使用原来的底层数组。如果原切片的容量足够大、append 就会直接修改原切片。
在这段代码中、由于原始切片 s 的容量足够大(原始切片 s 的容量为 5)、append 仍然修改了原始切片 s 的内容。切片的 s 和 s1 都指向相同的底层数组。
重点:
原切片 s 的容量没有改变:s 底层的数组仍然包含原来 s 的所有元素。
append 没有重新分配新的底层数组:由于原切片的容量足够、所以 append 在修改原底层数组时、并没有创建新的底层数组。因此原始切片 s 中的 4 仍然存在。
修改后 s 中的元素为 [0, 2, 3, 4, 4]:虽然你删除了 s[1] 这个元素、但 append 使得 s 的底层数组没有发生变化,因此原始的 4 元素仍然保留在切片中。
结论:操作有时会创建新的底层数组(如果容量不足)、但如果原切片的容量足够、append 直接修改原切片的底层数组。在这种情况下原切片 s 会保持原来的容量和数据、导致 s 显示为 [0, 2, 3, 4, 4],即最后一个 4 保留下来了。
3.5 新增元素
- package mainimport ( "fmt" "reflect" "unsafe")func main() { case1() case2() case3()}// case1 函数展示了使用 append 在切片末尾添加元素的行为func case1() { // 创建一个长度为 3,容量为 3 的切片 s1 := make([]int, 3, 3) // 向切片添加一个元素 1,append 返回一个新的切片 s1 = append(s1, 1) // 打印切片的底层结构 PrintSliceStruct(&s1) //1}// case2 函数展示了在原切片上使用 append 并打印切片结构的变化func case2() { // 创建一个长度为 3,容量为 4 的切片 s1 := make([]int, 3, 4) // 向切片添加一个元素 1,append 会扩展切片的长度 s2 := append(s1, 1) // 打印原切片 s1 和新切片 s2 的底层结构 PrintSliceStruct(&s1)//2 PrintSliceStruct(&s2)//3}// case3 函数与 case2 类似,展示了切片长度、容量变化的行为func case3() { // 创建一个长度为 3,容量为 3 的切片 s1 := make([]int, 3, 3) // 向切片添加一个元素 1,append 返回一个新的切片 s2 := append(s1, 1) // 打印原切片 s1 和新切片 s2 的底层结构 PrintSliceStruct(&s1)//4 PrintSliceStruct(&s2)//5}// PrintSliceStruct 打印切片的底层结构func PrintSliceStruct(s *[]int) { // 使用 reflect 和 unsafe 包将切片转换成 reflect.SliceHeader 结构体 ss := (*reflect.SliceHeader)(unsafe.Pointer(s)) // 打印切片的底层结构 fmt.Printf("slice struct: %+v, slice is %v\n", ss, *s)}
复制代码 控制台输出- slice struct: &{Data:1374390755328 Len:4 Cap:6}, slice is [0 0 0 1]slice struct: &{Data:1374390779936 Len:3 Cap:4}, slice is [0 0 0]slice struct: &{Data:1374390779936 Len:4 Cap:4}, slice is [0 0 0 1]slice struct: &{Data:1374390673552 Len:3 Cap:3}, slice is [0 0 0]slice struct: &{Data:1374390755376 Len:4 Cap:6}, slice is [0 0 0 1]
复制代码 使用 make([]int, 3, 3) 创建了一个长度为 3,容量为 3 的切片 s1,初始内容为 [0, 0, 0]。
然后会将元素 1 添加到切片的末尾、生成一个新的切片并返回。由于容量是 3、会自动扩容新的切片长度是 4
最后调用打印 s1 切片的底层结构。创建了一个长度为 3、容量为 4 的切片 s1
使用向切片添加元素 1、生成一个新切片 s2。由于 s1 的容量已足够、不会触发扩容。
通过打印切片 s1 和 s2 的底层结构。
s3 和 s4 参考上面
3.6 操作原来切片会影响新的切片吗
在 Go 中、切片是引用类型,这意味着当你创建一个新切片时,它实际上可能会指向同一个底层数组。因此,如果你修改了原切片(比如通过 append 或其他操作),它可能会影响到新切片,特别是在底层数组没有被重新分配的情况下。
切片和底层数组
切片(slice)是一个非常轻量级的抽象,它包含了三个部分:指向底层数组的指针、切片的长度和切片的容量。
当你对切片进行操作时(例如使用 append、copy 或直接修改),这些操作通常会影响到底层数组。
如果多个切片引用同一个底层数组,改变其中一个切片的内容可能会影响到其他切片,尤其是在没有扩容时。
append操作
当使用 append 函数时、如果切片的容量足够、append 会直接在原底层数组上操作、不会创建新的底层数组。在这种情况下、修改原切片的内容会影响到新切片、因为它们指向相同的底层数组。
如果容量不足、append 会创建一个新的底层数组、并将原切片的数据复制到新数组中、这时原切片和新切片就指向不同的底层数组了、它们互不影响。
例子:
没有扩容:- s1 := []int{1, 2, 3}
- s2 := s1 // s2 指向与 s1 相同的底层数组
- s1[0] = 100 // 修改 s1 中的第一个元素
- fmt.Println(s1) // 输出 [100, 2, 3]
- fmt.Println(s2) // 输出 [100, 2, 3]
复制代码 这里s1 和 s2 指向相同的底层数组,因此修改 s1 会影响到 s2。
扩容时:- s1 := []int{1, 2, 3}
- s2 := append(s1, 4) // s2 创建了新的底层数组
- s1[0] = 100 // 修改 s1 中的第一个元素
- fmt.Println(s1) // 输出 [100, 2, 3]
- fmt.Println(s2) // 输出 [1、2、3、 4]
复制代码 这里s2 创建了一个新的底层数组,因此修改 s1 不会影响 s2。
结论:
修改原切片会影响新切片:如果新切片是通过引用原切片的底层数组创建的(没有触发扩容)、修改原切片的内容会影响到新切片。
扩容时不影响:如果 append 或其他操作导致了扩容、原切片和新切片就会指向不同的底层数组、互不影响。
4.字节面试题
下面这道题的输出是什么- package main
- import "fmt"
- func main() {
- // 定义一个匿名函数 doAppend,用来执行 append 操作并打印切片的长度和容量
- doAppend := func(s []int) {
- s = append(s, 1) // 向切片中添加元素 1
- printLengthAndCapacity(s) // 打印切片的长度和容量
- }
- // 创建一个长度为 8,容量为 8 的切片 s
- s := make([]int, 8, 8)
-
- // 传递 s 的前 4 个元素(即 s[:4])到 doAppend
- doAppend(s[:4]) // 只传递前4个元素的切片
- // 打印原始切片 s 的长度和容量
- printLengthAndCapacity(s)
- // 传递整个切片 s 到 doAppend
- doAppend(s)
-
- // 打印原始切片 s 的长度和容量
- printLengthAndCapacity(s)
- }
- func printLengthAndCapacity(s []int) {
- fmt.Println()
- fmt.Printf("len=%d cap=%d \n", len(s), cap(s)) // 打印切片的长度和容量
- }
复制代码 len(s) 是切片的长度。
cap(s) 是切片的容量、表示切片底层数组的大小。- len=5 cap=8 len=8 cap=8 len=9 cap=16 len=8 cap=8
复制代码 调用s[:4] 是 s 切片的前 4 个元素、创建一个新的切片 [0, 0, 0, 0],长度为 4、容量为 8(因为它引用的是原切片的底层数组)。
在 doAppend 中、执行 append(s, 1)、这会向切片添加一个元素 1、导致切片的长度变为 5、容量保持为 8(因为它没有触发扩容)。
打印结果为:len=5 cap=8。
调用 doAppend(s):
这次传递整个 s 切片、长度为 8、容量为 8。
执行、这会向切片 s 添加一个元素 1。因为 s 的容量是 8、不能再容纳更多元素、因此会触发扩容、新的底层数组的容量将是原来的 2 倍、即 16、长度变为 9。
打印结果为:len=9 cap=16。
注意:
append 创建了一个新的底层数组、并返回了一个新的切片。如果你不把返回的新切片赋值回 s、原始切片 s 不会改变、仍然指向旧的底层数组。
由于 append(s 1) 返回的是一个新的切片、但并没有将它赋值回 s、所以原始切片 s 的长度和容量没有变化、仍然是 len=8 和 cap=8
如果改成将新切片赋值回s
- package main
- import "fmt"
- func main() {
- // 定义一个匿名函数 doAppend 用于向切片添加元素
- doAppend := func(s []int) {
- s = append(s, 1) // 向切片中添加元素 1
- printLengthAndCapacity(s) // 打印切片的长度和容量
- }
- // 定义一个匿名函数 doAppend 用于向切片添加元素
- doAppends := func(s []int) []int {
- s = append(s, 1) // 使用 append 向切片添加一个元素 1
- printLengthAndCapacity(s)
- return s
- }
- // 创建一个长度为 8,容量为 8 的切片 s
- s := make([]int, 8, 8)
- // 传递前 4 个元素的切片
- doAppend(s[:4]) // 只传递前4个元素的切片
- printLengthAndCapacity(s)
- // 传递整个切片 s
- s = doAppends(s) // 将返回的新切片赋值回 s
- printLengthAndCapacity(s)
- }
- func printLengthAndCapacity(s []int) {
- fmt.Println()
- fmt.Printf("len=%d cap=%d \n", len(s), cap(s))
- }
复制代码- len=5 cap=8 len=8 cap=8 len=9 cap=16 len=9 cap=16
复制代码
到此这篇关于Go语言Slice切片底层的实现的文章就介绍到这了,更多相关Go语言Slice切片内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:互联网
免责声明:如果侵犯了您的权益,请联系站长(1277306191@qq.com),我们会及时删除侵权内容,谢谢合作! |