Go 1.19.4 切片与子切片-Day 05

news2024/11/23 9:02:05

1. 切片

1.1 介绍

切片在Go中是一个引用类型,它包含三个组成部分:指向底层数组的指针(pointer)、切片的长度(length)以及切片的容量(capacity),这些信息共同构成了切片的“头(header)”。

切片是一个非常奇怪的集合体,它底层用的是数组,但它又能把数组值复制这个问题规避掉。
为啥底层是数组呢?因为它需要使用顺序表,因为使用索引访问,在顺序表中是最快的。

1.2 特点

它的特点如下:
(1)长度可以变,容量可变,长度和容量可以不一样,首次定义时,长度和容量相同。

长度:表示当前元素的数量
容量:表示最多可以定义多少个元素。
如切片长度3,容量5,含义为我切片中最多可以放5个元素,但当前只用了3个,还剩2个元素可以放置。
我把它理解为k8s中的request和limit。

(2)引用类型
切片之间引用(复制)的是header,并不是直接引用内存地址。

(3)底层基于数组

1.3 定义方式

1.3.1 方式一:字面量赋值定义

该方式适合小批量的定义,如果切片元素过多,就不太适合了。

package main

import "fmt"

func main() {
    // 错误的声明方式
    // var s0 = []int
    
	// 这就是定义一个切片,如果在[]中加上数字或者...,那就是一个数组
    // 这里的int可以是go中支持的任意数据类型,但元素类型必须一致
	var s0 = []int{1, 2, 3} // 该切片长度为3,容量为3
	fmt.Printf("%v\n%[1]T", s0)
}
=========调试结果=========
[1 2 3] // 光从输出结果来看,是无法分辨数组和切片
[]int // 打印值类型就可以,[]中为空,就表示切片

1.3.2 方式二:声明空切片(不推荐)

package main

import "fmt"

func main() {
	// 定义一个长度为0,容量为0的切片
	var s1 []int
	fmt.Printf("%T %[1]v %d %d", s1, len(s1), cap(s1))
}
=========调试结果=========
[]int [] 0 0

1.3.3 方式三:make(推荐)

make可以给内建容器开辟内存空间,比较适合用于多元素定义的场景。
并且make还能指定初始容量大小,减少频繁扩容。
但是注意,不同的数据类型使用make,参数含义是不一样的。

package main

import "fmt"

func main() {
	// 0,表示长度为0,目前由于没有元素,所以容量也为0。
	// 切片使用make,()中的第二个参数表示长度
	var s3 = make([]int, 0)
	fmt.Println(s3, len(s3), cap(s3))

	// 切片使用make,()中的第二个参数0表示长度,第三个参数5表示容量
	s4 := make([]string, 0, 5)
	fmt.Println(s4, len(s4), cap(s4))
}
=========调试结果=========
[] 0 0 // 长度为0,容量为0
[] 0 5 // 长度为0,容量为5

1.4 切片内存模型

切片的内存模型大致如下,还能称为切片的herdedr:
(1)pointer
存放的指向底层数组的指针。
这个指针指向切片实际引用的数组元素的起始位置。通过这个指针,切片能够访问和操作底层数组中的元素。

(2)len
存放当前切片的长度,这个长度决定了切片可以访问的底层数组元素的范围。

(3)cap
存放当前切片的容量,容量反映了切片可以增长元素的最大范围,即在不需要重新分配底层数组的情况下,可以向切片追加的元素数量。

由于切片需要使用顺序表,所以它的底层其实还是依赖数组的。
但是数组一旦定死它的长度是不可变的,而切片的长度和容量都可变,那数组的长度不够咋办呢?
切换底层数组,当切片需要扩容,但底层数组长度又不够的时候,go会废弃这个老的底层数组,再创建一个新的满足切片扩容长度的底层数组。
在这里插入图片描述

1.4.1 切片元素内存地址理解

package main

import "fmt"

func main() {
	var s0 = []int{1, 2, 3}
	fmt.Printf("%p %p\n", &s0, &s0[0])
    // &s0,表示的是当前这个结构体(切片)的内存地址(header地址)。
	// &s0[0],表示的是当前这个切片底层数组的第一个元素的内存地址,也是底层数组的首地址。
}
=========调试结果=========
0xc000008078 0xc000010168

1.4.2 追加内容到切片(append)

append内置函数,用于在切片的尾部追加元素,并且不会修改当前切片的header,因为它总是会返回一个新的header(至于header内容是否改变,取决于操作的切片是新还是旧)。
如果是基于老切片新增元素给新切片,则header可能会发生变化,也就是说pointer、len、cap都有可能会发生变化。
增加元素后,有可能超过当前切片容量,导致切片扩容(切片扩容容量为扩容前已存在元素的倍数)。
注意append只能用于切片。

package main

import "fmt"

func main() {
	var s0 = []int{1, 2, 3}
	fmt.Printf("%p %p\n", &s0, &s0[0])

	// append(s0, 11),表示对s0进行尾部元素追加,追加完毕后又写入到s0
	s0 = append(s0, 11)
	fmt.Println(s0, &s0[0])
}
=========调试结果=========
0xc000008078 0xc000010168
// 11就是追加的内容,并且追加后,底层数组的首地址也发生了改变
// 这是符合上面的推断的
[1 2 3 11] 0xc00000e3c0
1.4.2.1 切片长度与容量
package main

import "fmt"

func main() {
    // 切片长度为3,容量为5
	var s0 = make([]int, 3, 5)
	fmt.Printf("切片内存地址:%p\n底层数组首地址:%p\n切片元素数量:%d\n切片容量:%v\n切片元素:%v", &s0, &s0[0], len(s0), cap(s0), s0)
}
=========调试结果=========
切片内存地址:0xc000116060
底层数组首地址:0xc000142030
切片元素数量:3
切片容量:5
切片元素:[0 0 0]

基于老切片追加元素到新切片,观察新老切片的变化。

// 上面s0切片还是3个0值,下面我给他调整一下
package main

import "fmt"

func main() {
	var s0 = make([]int, 3, 5)
	fmt.Printf("切片内存地址:%p\n底层数组首地址:%p\n切片元素数量:%d\n切片容量:%v\n切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)
	fmt.Println("----------------------------------")

    // 向s0追加两个元素,得到新的切片s1
	s1 := append(s0, 1, 2)
	fmt.Println(s0, len(s0), cap(s0))
	fmt.Println(s1, len(s1), cap(s1))
}
=========调试结果=========
切片内存地址:0xc0000aa060
底层数组首地址:0xc0000d8030
切片元素数量:3
切片容量:5
切片元素:[0 0 0]
----------------------------------
// 看这部分
[0 0 0] 3 5 // 这是s0
[0 0 0 1 2] 5 5 // 这是s1

为什么s0的长度和容量与s1不一样?
这就不得不再说下切片的herdedr了,首先最开始用make定义切片的时候,var s0 = make([]int, 3, 5),这个切片中只存储了3个0元素,但由于容量为5,实际上还能增加2个元素。
所以追加两个元素后(​​s0​​原本长度为3,追加后长度为5),总长度并没有超过原切片的容量(5),所以​​append​​操作是在原切片​​s0​​的底层数组上进行的,并且​​s1​​和​​s0​​共享同一个底层数组。但是,​​s1​​和​​s0​​是两个不同的切片头(header),因为它们有不同的长度。

那这里思考一个问题,s0和s1的底层数组是否相同?
看下面的代码:

package main

import "fmt"

func main() {
	// 定义一个长度为3,容量为5的切片
	var s0 = make([]int, 3, 5)
	fmt.Printf("切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)
	fmt.Println("----------------------------------")

	// 向s0追加两个元素,得到新的切片s1
	s1 := append(s0, 1, 2)
	// fmt.Println(s0, len(s0), cap(s0))
	// fmt.Println(s1, len(s1), cap(s1))

	fmt.Printf("切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
}
=========调试结果=========
切片内存地址:0xc000080048 底层数组首地址:0xc0000aa030 切片元素数量:3 切片容量:5 切片元素:[0 0 0]
----------------------------------
切片内存地址:0xc000080078 底层数组首地址:0xc0000aa030 切片元素数量:5 切片容量:5 切片元素:[0 0 0 1 2]

通过上面的返回可以看到,s0切片和s1切片的header(内存地址)不同,但底层数组地址完全一样,究其原因就是因为底层数组的长度是满足元素新增的,所以实际上两个切片都是引用的同一个数组(数据是存在同一个内存空间中的)。

既然底层是同一个数组,为什么s0和s1显示的内容不同?
可以把切片的长度当成一个窗帘,底层数组实际上就是存储着00012,但由于s0受到长度3的限制,所以我们是看不到超过长度3的内容的。

为啥两个切片的header不同呢?
因为两个切片的元素数量不同,所以s1 := append(s0, 1, 2)插入元素后返回值给s1时,header中的len被更新了,所以header看着不一样,其实简单理解,s0和s1都是一个独立的切片,所以header肯定不一样,虽然它们底层引用的都是相同的数组。

1.4.2.2 切片容量溢出

这里主要讲一下,切片容量溢出后,底层到底是怎么做的。
主要看下面新增的s3切片:

package main

import "fmt"

func main() {
	// 定义一个长度为3,容量为5的切片
	var s0 = make([]int, 3, 5)
	fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)
	fmt.Println("----------------------------------")

	// 向s0追加两个元素,得到新的切片s1。
	s1 := append(s0, 1, 2)
	fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
	fmt.Println("----------------------------------")

	s2 := append(s0, -1)
	fmt.Printf("s2 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)
	fmt.Println("----------------------------------")
    
    // 向s2追加三个元素,得到新的切片s3
	s3 := append(s2, 3, 4, 5)
	fmt.Printf("s3 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
=========调试结果=========
s0 切片内存地址:0xc000008078 底层数组首地址:0xc00000e3c0 切片元素数量:3 切片容量:5 切片元素:[0 0 0]
----------------------------------
s1 切片内存地址:0xc0000080a8 底层数组首地址:0xc00000e3c0 切片元素数量:5 切片容量:5 切片元素:[0 0 0 1 2]
----------------------------------
s2 切片内存地址:0xc0000080d8 底层数组首地址:0xc00000e3c0 切片元素数量:4 切片容量:5 切片元素:[0 0 0 -1]
----------------------------------
s3 切片内存地址:0xc000008108 底层数组首地址:0xc000012230 切片元素数量:7 切片容量:10 切片元素:[0 0 0 -1 3 4 5]

上述代码中,通过向s2追加三个元素,得到新的切片s3。
具体的实现逻辑大概是这样:
s2底层数组容量为5,长度为4,append要新增3个,超了2个,触发扩容,于是向系统申请一块新的连续(顺序表)的内存空间,然后将s2底层数组中已有的数据复制过来,再把要追加的元素写入,最终得到一个新的底层数组,并且append还会返回一个全新的header给到s3,其中pointer指向新的底层数组、切片长度为7、切片容量为10(系统会自动冗余一些空间,后续讲扩容策略)。

1.5 切片的扩容机制

官方文档:​​https://go.dev/src/runtime/slice.go​​

(老版本)实际上,当扩容后的cap<1024时,扩容翻倍,容量变成之前的2倍;当cap>=1024时,变成之前的1.25倍(扩容前已存在元素的倍数)。
(新版本1.18+)阈值变成了256,当扩容后的cap<256时,扩容翻倍,容量变成之前的2倍(扩容前已存在元素的倍数);当cap>=256时, newcap += (newcap + 3*threshold) / 4 计算后就是 newcap = newcap +
newcap/4 + 192 ,即1.25倍后再加192。

扩容是创建新的底层数组,把原内存数据拷贝到新内存空间,然后在新内存空间上执行元素追加操作。

切片频繁扩容成本非常高(元素越多,复制时间越长),所以尽量早估算出使用的大小,一次性给够,建议使用make。常用make([]int, 0, 100) 。

header复制也会消耗资源,但是很少。
如:var s1 = s0,这种就是header结构体复制

思考一下:如果 s1 := make([]int, 3, 100) ,然后对s1进行append元素,会怎么样?
当追加的元素不超过切片容量时,只有切片长度会变,其他不变。
如果超过了容量,那么就会触发扩容。
在这里插入图片描述

1.6 引用类型

在Go语言中,引用类型(Reference Types)是指那些在赋值、作为函数参数传递或作为函数返回值时,传递的是指针(即内存地址)的类型,而不是值本身。
这意味着,当操作引用类型的变量时,实际上是在操作其指向的内存位置上的数据。
但严格意义上来说,复制的是header。
Go语言中的引用类型包括切片(slices)、映射(maps)、通道(channels)、接口(interfaces)、函数类型以及指向它们的指针。

1.6.1 思考以下代码切片之间是否发生了复制

package main

import "fmt"

func main() {
	var s0 = []int{1, 3, 5}
	fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)

	s1 := s0
	fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
}
=========调试结果=========
s0 切片内存地址:0xc000008078 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]
s1 切片内存地址:0xc0000080a8 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]

通过返回结果可以得出,只是把切片赋值给另一个新切片,只有header地址会改变,header中的pointer、len、cap都不会变。

这说明什么?说明s0和s1之间,只复制了header结构体,但header中的pointer、len、cap都没变。

如果把s1切片的元素修改,s0切片会改变吗?

package main

import "fmt"

func main() {
	var s0 = []int{1, 3, 5}
	// fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)

	s1 := s0
	// fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	s1[0] = 100
	fmt.Println(s0, s1)
}
=========调试结果=========
[100 3 5] [100 3 5]

表面上看,操作s1就好像在操作s0,有点类似复制了切片的内存地址,通过地址操作两个切片一起变,但实际上还是因为两个切片共用同一个底层数组。

1.6.2 使用函数传参是否会发生复制

package main

import "fmt"

func showAddr(s2 []int) { // 新增函数
	fmt.Printf("s2 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)
}

func main() {
	var s0 = []int{1, 3, 5}
	fmt.Printf("s0 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s0, &s0[0], len(s0), cap(s0), s0)

	s1 := s0
	fmt.Printf("s1 切片内存地址:%p 底层数组首地址:%p 切片元素数量:%d 切片容量:%v 切片元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	s1[0] = 100
	// fmt.Println(s0, s1)

	showAddr(s0) // 函数传参
}
=========调试结果=========
s0 切片内存地址:0xc000008078 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]
s1 切片内存地址:0xc0000080a8 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[1 3 5]
s2 切片内存地址:0xc0000080d8 底层数组首地址:0xc000010168 切片元素数量:3 切片容量:3 切片元素:[100 3 5]

通过结果得出,只有header结构体发生了复制,但header中存储的pointer、len、cap不变。

1.7 总结

Go语言中全都是值拷贝(复制),如整型、数组这样的类型的值是完全复制,slice、map、channel、interface、function这样的引用类型也是值拷贝,不过复制的是标头值。

2 . 子切片

2.1 介绍

切片可以通过指定索引区间获得一个子切片,格式为slice[start:end],规则就是前包后不包,对应元素的索引。

2.2 子切片特点

子切片(slice)是基于底层数组的一个视图或者窗口。
当从一个已有的切片中创建子切片时,实际上是在共享同一个底层数组,而不是创建一个新的、独立的数组。因此,子切片的创建本身不会导致底层数组的扩容。
但是,如果使用append追加,则是有可能触发扩容的。

2.3 子切片语法

slice[start:end]
start:不写默认为0。
end:不写话,默认为切片长度。
注意:指定start和end时,不能超过切片的容量。

2.4 子切片示例

2.4.1 示例一:完全复制header

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	// 把s1切片赋值给s2
	s2 := s1 // 本质上就是在复制header
	fmt.Printf("s2的内存地址:%p|s2的底层数组首地址:%p|s2的长度:%d|s2的容量:%d|s2的元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)

	// 开始子切片
	s3 := s1[:] //构建一个新的header,但不会新建数组
	fmt.Printf("s3的内存地址:%p|s3的底层数组首地址:%p|s3的长度:%d|s3的容量:%d|s3的元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
===========调试结果===========
s1的内存地址:0xc0000aa060|s1的底层数组首地址:0xc0000d8030|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s2的内存地址:0xc0000aa090|s2的底层数组首地址:0xc0000d8030|s2的长度:5|s2的容量:5|s2的元素:[10 30 50 70 90]
s3的内存地址:0xc0000aa0c0|s3的底层数组首地址:0xc0000d8030|s3的长度:5|s3的容量:5|s3的元素:[10 30 50 70 90]

通过上面的代码,可以看到s3子切片后,结果和之前的相同,说明了什么?
子切片和原来的切片使用的底层数组也是同一个。

2.4.2 示例二:偏移切片

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)
    
    // 首地址发生变化,切偏移一个元素,最终的长度和容量都-1
	s4 := s1[1:]
	fmt.Printf("s4的内存地址:%p|s4的底层数组首地址:%p|s4的长度:%d|s4的容量:%d|s4的元素:%v\n", &s4, &s4[0], len(s4), cap(s4), s4)

}
===========调试结果===========
s1的内存地址:0xc000008078|s1的底层数组首地址:0xc00000e3c0|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s4的内存地址:0xc0000080a8|s4的底层数组首地址:0xc00000e3c8|s4的长度:4|s4的容量:4|s4的元素:[30 50 70 90]

看结果:
s1的底层数组首地址:0xc00000e3c0
s4的底层数组首地址:0xc00000e3c8
是不是以为底层数组变了?错,子切片过程中,只要没有append操作,底层数组依然还是同一个。
之所以一个首地址是3c0,一个是3c8,是因为int类型就占用8个字节。
并且s4 := s1[1:],意思是偏移了一个元素(把第一个元素挡住了,看不到了),所以此时的首地址就变成了第二个元素的内存地址。
并且由于偏移了一个元素,所以子切片的容量就为4,长度呢?长度没有指定,所以就从偏移处直到末尾,为4。

2.4.3 示例三:指定start和end

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

    // s1[1:4],展示元素索引1,2,3的元素。
	s5 := s1[1:4]
	fmt.Printf("s5的内存地址:%p|s5的底层数组首地址:%p|s5的长度:%d|s5的容量:%d|s5的元素:%v\n", &s5, &s5[0], len(s5), cap(s5), s5)

}
===========调试结果===========
s1的内存地址:0xc00009a060|s1的底层数组首地址:0xc0000c8030|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s5的内存地址:0xc00009a090|s5的底层数组首地址:0xc0000c8038|s5的长度:3|s5的容量:4|s5的元素:[30 50 70]

s5此处的切片长度为:3
s5此处的切片容量为:4
那这个长度和容量是怎么计算出来的?
子切片长度计算方式:end减去start
子切片容量计算方式:从偏移量(start索引)开始到切片底层数组的最后一个元素。

2.4.4 示例四:start和end相同

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

    // 该子切片会复制一个新的header,偏移一个元素,子切片长度为0,容量为4
	s7 := s1[1:1] // 子切片元素超界了,这里是不能显示的
	fmt.Printf("s7的内存地址:%p|s7的底层数组首地址:%p|s7的长度:%d|s7的容量:%d|s7的元素:%v\n", &s7, &s7[0], len(s7), cap(s7), s7)

}

注意看s1[1:1],这里实际上已经超界了,长度为0,容量为4,如下图,并且执行的时候会报错。
在这里插入图片描述

然后基于现在的代码,对s7进行append操作,看看会发生什么。

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的长度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)

	s7 := s1[1:1]
    fmt.Printf("s7的长度:%d|s7的容量:%d|s7的元素:%v\n", len(s7), cap(s7), s7)
	s7 = append(s7, 300, 400)
	fmt.Printf("s1的长度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)
	fmt.Printf("s7的长度:%d|s7的容量:%d|s7的元素:%v\n", len(s7), cap(s7), s7)

}
===========调试结果===========
s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s7的长度:0|s7的容量:4|s7的元素:[]
s1的长度:5|s1的容量:5|s1的元素:[10 300 400 70 90]
s7的长度:2|s7的容量:4|s7的元素:[300 400]

可以看到,最开始s7长度为0(啥也看不到了),容量为4,append后长度变成了2,容量不变。
并且由于s7和s1共享同一个底层数组,所以对应s1切片中索引1和2的元素也被改变了。
为什么是索引1和2?
因为最开始s7 := s1[1:1],这里start是从1开始的,对应的就是s1切片元素中的索引1。
在这里插入图片描述

再来看一个特殊示例

package main

import "fmt"

func main() {
	// 声明并初始化一个长度和容量都为5的切片
	s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4
	fmt.Printf("s1的长度:%d|s1的容量:%d|s1的元素:%v\n", len(s1), cap(s1), s1)

    s9 := s1[5:5] //长度为0,容量为0,类似[]int{}定义方式
	fmt.Printf("s9的长度:%d|s9的容量:%d|s9的元素:%v\n", len(s9), cap(s9), s9)
}
===========调试结果===========
s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s9的长度:0|s9的容量:0|s9的元素:[]

为什么还能写成s9 := s1[5:5]?按索引来算不是超界了吗?
注意:指定start和end时,除了能使用元素对应的索引,还能够使用的最大值是切片的容量,s1切片的容量是5。
在这里插入图片描述

2.4.5 子切片总结

可以看出,上面所有示例操作都是从同一个底层数组上取的段,所以子切片和原始切片共用同一个底层数组。

  • start默认为0,end默认为len(slice)即切片长度,明确定义时可以使用的最大值为切片的容量。
  • 通过指针(切片内存地址)确定底层数组从哪里开始共享。
  • 切片长度计算方法是end - start。
  • 切片容量计算方式是底层数组从偏移的元素(start)到结尾还有几个元素。

2.5 切片总结

  1. 使用slice[start:end]表示切片,切片长度为end-start,前包后不包。
  2. start缺省(不写),表示从索引0开始。
  3. end缺省(不写),表示直接取到末尾,包含最后一个元素,特别注意这个值是len(slice)即切片长度,不是容量,如a1[5:]相当于a1[5:len(a1)]
  4. start和end都缺省,表示从头到尾。
  5. start和end同时给出,要求end >= start。
  6. start、end最大都不可以超过容量值。
  7. 假设当前容量是8,长度为5,有以下情况:
    a1[:8],可以,end最多写成8(因为后不包),a1[:9]不可以。
    a1[8:],不可以,end缺省为5,等价于a1[8:5]。
    a1[8:8],可以,但这个切片容量和长度都为0了。
    a1[7:7],可以,但这个切片长度为0,容量为1。
    a1[0:0],可以,但这个切片长度为0,容量为8。
    a1[:8],可以,这个切片长度为8,容量为8,这8个元素都是原序列的。
    a1[1:5],可以,这个切片长度为4,容量为7,相当于跳过了原序列第一个元素。
  8. 切片刚产生时,和原序列(数组、切片)开始共用同一个底层数组,但是每一个切片都自己独立保存着指针、cap和len。
  9. 一旦一个切片扩容,就和原来共用一个底层数组的序列分道扬镳,从此陌路。

3. 对数组进行切片

数组也可以切片,但是会生成新的切片

package main

import "fmt"

func main() {
	// 在[]中加个5,就变成了长度和容量都为5的数组
	s1 := [5]int{10, 30, 50, 70, 90}
	fmt.Printf("s1的内存地址:%p|s1的底层数组地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)

	// 数组拷贝,多一个副本出来,元素完全相同
	s2 := s1
	fmt.Printf("s2的内存地址:%p|s2的底层数组地址:%p|s2的长度:%d|s2的容量:%d|s2的元素:%v\n", &s2, &s2[0], len(s2), cap(s2), s2)

	s3 := s1[:]//这个切片操作,会产生一个新的底层数组吗?
	fmt.Printf("s3的内存地址:%p|s3的底层数组地址:%p|s3的长度:%d|s3的容量:%d|s3的元素:%v\n", &s3, &s3[0], len(s3), cap(s3), s3)
}
===========调试结果===========
s1的内存地址:0xc0000d8030|s1的底层数组地址:0xc0000d8030|s1的长度:5|s1的容量:5|s1的元素:[10 30 50 70 90]
s2的内存地址:0xc0000d80c0|s2的底层数组地址:0xc0000d80c0|s2的长度:5|s2的容量:5|s2的元素:[10 30 50 70 90]
s3的内存地址:0xc0000aa060|s3的底层数组地址:0xc0000d8030|s3的长度:5|s3的容量:5|s3的元素:[10 30 50 70 90]

可与看到,对数组进行切片后,切片的底层数组其实就是s1数组,说明对数组切片,不会诞生一个新的底层数组。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1794599.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

el-input添加clearable属性 输入内容时会直接撑开

<el-inputclearablev-if"item.type number || item.type text":type"item.type":placeholder"item.placeholder":prefix-icon"item.icon || "v-model.trim"searchform[item.prop]"></el-input>解决方案 添加c…

inflight 守恒拥塞控制的稳定性

只要系统形成 E_best max(bw / delay) 共识&#xff0c;系统就是稳定的。 设两条流 f1&#xff0c;f2 共享瓶颈链路&#xff0c;用 cwnd 约束 inflight&#xff0c;其 cwnd 分别为 x&#xff0c;y&#xff0c;用简单的微分方程建模&#xff1a; d x d t c − b ∗ x − a ∗…

TCP/IP(网络编程)

一、网络每一层的作用 &#xff0a;网络接口层和物理层的作用&#xff1a;屏蔽硬件的差异&#xff0c;通过底层的驱动&#xff0c;会提供统一的接口&#xff0c;供网络层使用 &#xff0a;网络层的作用&#xff1a;实现端到端的传输 &#xff0a;传输层:数据应该交给哪一个任…

区块链(Blockchain)调查研究

文章目录 1. 区块链是什么&#xff1f;2. 区块链分类和特点3. 区块链核心关键技术3.1 共识机制3.2 密码学技术3.4 分布式存储3.5 智能合约 4. 区块链未来发展趋势5. 区块链 Java 实现小案例 1. 区块链是什么&#xff1f; 区块链是分布式数据存储、点对点传输、共识机制、加密算…

TPC-H建表语句(MySQL语法)

TPC-H测试集介绍 TPC-H&#xff08;Transaction Processing Performance Council, Standard Specification, Decision Support Benchmark, 简称TPC-H&#xff09;是一个非常权威数据库基准测试程序&#xff0c;由TPC组织制定。 TPC-H定义了一个包含8个表的模式&#xff08;Sc…

Github上一款开源、简洁、强大的任务管理工具:Condution

Condution 是一款开源任务管理工具&#xff0c;它以简洁易用、功能强大著称。它旨在为用户提供一个简单高效的平台&#xff0c;帮助他们管理日常任务、提高工作效率。 1. Condution 的诞生背景 现如今&#xff0c;市面上存在着许多任务管理软件&#xff0c;但它们往往价格昂贵…

芒果YOLOv8改进169:即插即用 | 秩引导的块设计核心CIB结构,设计一种秩引导的块设计方案,旨在通过紧凑型架构设计减少被显示为冗余的阶段的复杂性

💡🚀🚀🚀本博客 秩引导的块设计,设计了一种秩引导的块设计方案,旨在通过紧凑型架构设计减少被显示为冗余的阶段的复杂性 :内含源代码改进 适用于 YOLOv8 按步骤操作运行改进后的代码即可 文章目录 即插即用|秩引导的块设计|最新改进 YOLOv8 代码改进论文理论YOLO…

山洪灾害监测预警系统守护生命安全的新利器

一、概述 我国地域辽阔&#xff0c;地形地貌条件复杂且气候类型多样&#xff0c;每年5—9月为山洪灾害多发期&#xff0c;6—8月主汛期山洪及次生地质灾害更为集中。由于地理位置、气候条件、地貌特征、社会经济发展水平等成灾环境的不同&#xff0c;山洪灾害在发生时间、空间和…

Mixly UDP局域网收发数据

一、开发环境 软件&#xff1a;Mixly 2.0在线版 硬件&#xff1a;ESP32-C3&#xff08;立创实战派&#xff09; 固件&#xff1a;ESP32C3 Generic(UART) 测试工具&#xff1a;NetAssist V5.0.1 二、实现功能 ESP32作为wifi sta连接到路由器&#xff0c;连接成功之后将路由器…

仅49天!中科院2区SCI,发文量超2W,征稿范围广!

【欧亚科睿学术】 &#xff08;一&#xff09;期刊简介概况 【出版社】SPRINGER出版社 【期刊概况】IF&#xff1a;4.0-5.0&#xff0c;JCR2区&#xff0c;中科院2区 【版面类型】正刊&#xff0c;仅10篇版面 【预警情况】2020-2024年无预警记录 【收录年份】2008年被WOS…

【git】TortoiseGitPlink Fatal Error 解决方法

背景 使用 TortoiseGit报错&#xff1a; TortoiseGitPlink Fatal Error No supported authentication methods available (server sent: publickey) 解决方法 1、有很多是重置git的秘钥解决的 2、重置ssh工具

如何理解与学习数学分析——第二部分——数学分析中的基本概念——第8章——可微性

第2 部分&#xff1a;数学分析中的基本概念 (Concepts in Analysis) 8. 可微性(Differentiability) 本章讨论梯度(gradients)/斜率(slopes)和切线(tangent)&#xff0c;指出常见的误解并解释如何避免这些误解。将可微性的定义与图形表示联系起来&#xff0c;展示如何将其应用…

[leetcode hot 150]第一百三十七题,只出现一次的数字Ⅱ

题目&#xff1a; 给你一个整数数组 nums &#xff0c;除某个元素仅出现 一次 外&#xff0c;其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。 由于需要常数级空间和线性时间复杂度…

论文Compiler Technologies in Deep Learning Co-Design: A Survey分享

目录 标题摘要引言背景深度学习软件和硬件的发展不同时期的协同设计深度学习协同设计系统神经网络架构设计和优化协同设计技术 用于协同设计的深度学习系统中的编译技术深度学习编译器TVM 生态系统和MLIR生态系统IR转换和优化代码生成运行时和执行模式 Buddy-Compiler: 一个针对…

[C++]基于C++opencv结合vibe和sort tracker实现高空抛物实时检测

【vibe算法介绍】 ViBe算法是一种高效的像素级视频背景建模和前景检测算法。以下是对该算法的详细介绍&#xff1a; 一、算法原理 ViBe算法的核心思想是通过为每个像素点存储一个样本集&#xff0c;利用该样本集与当前像素值进行比较&#xff0c;从而判断该像素是否属于背景…

问题:合规电动自行车国家标准是() #学习方法#媒体#经验分享

问题&#xff1a;合规电动自行车国家标准是&#xff08;&#xff09; A&#xff0e;必须有脚踏能实现人力骑行 B&#xff0e;最高设计车速不大于25km/h C&#xff0e;整车质量不大于55kg D&#xff0e;电机输出功率不大于240w 参考答案如图所示

【Python报错】已解决ModuleNotFoundError: No module named ‘timm’

成功解决“ModuleNotFoundError: No module named ‘timm’”错误的全面指南 一、引言 在Python编程中&#xff0c;经常会遇到各种导入模块的错误&#xff0c;其中“ModuleNotFoundError: No module named ‘timm’”就是一个典型的例子。这个错误意味着你的Python环境中没有安…

Windows系统中不同Java版本共存

Windows系统中不同Java版本共存的方法 在Windows系统中&#xff0c;有时我们需要同时运行多个Java应用&#xff0c;而这些应用可能依赖于不同版本的Java Development Kit (JDK) 或 Java Runtime Environment (JRE)。为了实现这种需求&#xff0c;我们需要在Windows中配置多个J…

『 Linux 』内存管理与文件系统

文章目录 交换分区页与页框(页帧)交换分区与内存之间的交换操作系统如何管理内存物理地址转换页号与页内偏移量 内存管理,文件系统与文件管理之间的联系 交换分区 在Linux的安装过程中,用户将会被提示创建一个交换分区; 这是一个特殊的分区,其大小可以由用户根据系统内存需求和…

Apache POI对Excel进行读写操作

1、什么是Apache POI ​ Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是&#xff0c;我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。一般情况下&#xff0c;POI 都是用于操作 Excel 文件。 Apache POI 的应用场景&…