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

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

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

1.3.3 方式三:make(推荐)

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

package mainimport "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 mainimport "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 mainimport "fmt"func main() {var s0 = []int{1, 2, 3}fmt.Printf("%p %p\n", &s0, &s0[0])// append(s0, 11),表示对s0进行尾部元素追加,追加完毕后又写入到s0s0 = append(s0, 11)fmt.Println(s0, &s0[0])
}
=========调试结果=========
0xc000008078 0xc000010168
// 11就是追加的内容,并且追加后,底层数组的首地址也发生了改变
// 这是符合上面的推断的
[1 2 3 11] 0xc00000e3c0
1.4.2.1 切片长度与容量
package mainimport "fmt"func main() {// 切片长度为3,容量为5var 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 mainimport "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追加两个元素,得到新的切片s1s1 := 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 mainimport "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追加两个元素,得到新的切片s1s1 := 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 mainimport "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追加三个元素,得到新的切片s3s3 := 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 mainimport "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 := s0fmt.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 mainimport "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] = 100fmt.Println(s0, s1)
}
=========调试结果=========
[100 3 5] [100 3 5]

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

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

package mainimport "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 := s0fmt.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 mainimport "fmt"func main() {// 声明并初始化一个长度和容量都为5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 把s1切片赋值给s2s2 := s1 // 本质上就是在复制headerfmt.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 mainimport "fmt"func main() {// 声明并初始化一个长度和容量都为5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 首地址发生变化,切偏移一个元素,最终的长度和容量都-1s4 := 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 mainimport "fmt"func main() {// 声明并初始化一个长度和容量都为5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4fmt.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 mainimport "fmt"func main() {// 声明并初始化一个长度和容量都为5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4fmt.Printf("s1的内存地址:%p|s1的底层数组首地址:%p|s1的长度:%d|s1的容量:%d|s1的元素:%v\n", &s1, &s1[0], len(s1), cap(s1), s1)// 该子切片会复制一个新的header,偏移一个元素,子切片长度为0,容量为4s7 := 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 mainimport "fmt"func main() {// 声明并初始化一个长度和容量都为5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4fmt.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 mainimport "fmt"func main() {// 声明并初始化一个长度和容量都为5的切片s1 := []int{10, 30, 50, 70, 90} // 索引范围[0,4] 0到4fmt.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 mainimport "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 := s1fmt.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.mzph.cn/web/23313.shtml

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

相关文章

JavaWeb_SpringBootWeb案例

环境搭建&#xff1a; 开发规范 接口风格-Restful&#xff1a; 统一响应结果-Result&#xff1a; 开发流程&#xff1a; 第一步应该根据需求定义表结构和定义接口文档 注意&#xff1a; 本文代码从上往下一直添加功能&#xff0c;后面的模块下的代码包括前面的模块&#xff0c…

Xmind Pro 2024 专业版激活码(附下载链接)

说到思维导图&#xff0c;就不能不提 Xmind。这是一款优秀的思维导图工具&#xff0c;拥有着丰富的导图模板&#xff0c;漂亮的界面和配色&#xff0c;以及各种各样的创意工具。 新架构速度更快 采用全新 Snowdancer 引擎&#xff0c;一种堪称「黑科技」的先进图形渲染技术。…

翘首以盼的抗锯齿

Antialiasing 实际的图形学中是怎么实现反走样的呢&#xff1f; 我们不希望实际产出的图形有锯齿效果&#xff0c;那怎么办呢&#xff1f; 从采样的理论开始谈起吧 Simpling theory 照片也是一种采样&#xff0c;把景象打散成像素放到屏幕上的过程&#xff1a; 还可以在不…

21 - 即时食物配送 II(高频 SQL 50 题基础版)

21 - 即时食物配送 II -- sum(if(order_datecustomer_pref_delivery_date,1,0))/count(*)sum(order_datecustomer_pref_delivery_date)/count(*) -- count(*),表示数据的行数&#xff0c;如果有分组&#xff0c;为分组后数据的行数select round(100*sum(if(order_datecustomer_…

原来Stable Diffusion是这样工作的

stable diffusion是一种潜在扩散模型&#xff0c;可以从文本生成人工智能图像。为什么叫做潜在扩散模型呢&#xff1f;这是因为与在高维图像空间中操作不同&#xff0c;它首先将图像压缩到潜在空间中&#xff0c;然后再进行操作。 在这篇文章中&#xff0c;我们将深入了解它到…

达摩院重大“遗产”!fluxonium量子比特初始化300纳秒且保真度超过99%

通用量子计算机开发的主要挑战之一是制备量子比特。十多年来&#xff0c;研究人员在构建量子计算机的过程中主要使用了transmon量子比特&#xff0c;这也是迄今为止商业上最成功的超导量子比特。 但与业界多数选择transmon量子比特不同&#xff0c;&#xff08;前&#xff09;…

Python文本处理利器:jieba库全解析

文章目录 Python文本处理利器&#xff1a;jieba库全解析第一部分&#xff1a;背景和功能介绍第二部分&#xff1a;库的概述第三部分&#xff1a;安装方法第四部分&#xff1a;常用库函数介绍1. 精确模式分词2. 全模式分词3. 搜索引擎模式分词4. 添加自定义词典5. 关键词提取 第…

服务器遭遇UDP攻击时的应对与解决方案

UDP攻击作为分布式拒绝服务(DDoS)攻击的一种常见形式&#xff0c;通过发送大量的UDP数据包淹没目标服务器&#xff0c;导致网络拥塞、服务中断。本文旨在提供一套实用的策略与技术手段&#xff0c;帮助您识别、缓解乃至防御UDP攻击&#xff0c;确保服务器稳定运行。我们将探讨监…

最新PHP众筹网站源码 支持报名众筹+商品众筹+公益众筹等多种众筹模式 含完整代码包和部署教程

在当今互联网飞速发展的时代&#xff0c;众筹模式逐渐成为了创新项目、商品销售和公益活动融资的重要渠道。分享一款最新版的PHP众筹网站源码&#xff0c;支持报名众筹、商品众筹和公益众筹等多种众筹模式。该源码包含了完整的代码包和详细的部署教程&#xff0c;让新手也可以轻…

利用医学Twitter进行病理图像分析的视觉-语言基础模型| 文献速递-视觉通用模型与疾病诊断

Title 题目 A visual–language foundation model for pathology image analysis using medical Twitter 利用医学Twitter进行病理图像分析的视觉-语言基础模型 01 文献速递介绍 缺乏公开可用的医学图像标注是计算研究和教育创新的一个重要障碍。同时&#xff0c;许多医生…

自动化测试-Selenium(一),简介

自动化测试-Selenium 1. 什么是自动化测试 1.1 自动化测试介绍 自动化测试是一种通过自动化工具执行测试用例来验证软件功能和性能的过程。与手动测试不同&#xff0c;自动化测试使用脚本和软件来自动执行测试步骤&#xff0c;记录结果&#xff0c;并比较预期输出和实际输出…

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

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

Navicate 导入导出数据库

导出数据库 找地方存在来&#xff0c;别忘了放在那里。 新建一个数据库&#xff0c;记得要和导出数据库的 字符集与排序规则 相同 打开数据库后&#xff0c;我们选择它&#xff08;就是单击它&#xff09;然后右键打开菜单-运行sql文件 找到刚才存储的位置&#xff0c;开始 &a…

大中小面积紫外光老化加速试验机装置

高低温试验箱,振动试验台,紫外老化试验箱,氙灯老化试验箱,沙尘试验箱,箱式淋雨试验箱,臭氧老化试验箱,换气老化试验箱,电热鼓风干燥箱,真空干燥箱&#xff0c;超声波清洗机&#xff0c;盐雾试验箱 一、产品用途 紫外光加速老化试验装置采用荧光紫外灯为光源,通过模拟自然阳光中…

oracle报错ORA-01940: cannot drop a user that is currently connected解决方法

目录 一.原因 二.解决方法 1.查询活动会话 2.记下SID和SERIAL# 3.断开会话 4.删除用户 一.原因 ORA-01940代表你正在删除一个有活动会话的用户 二.解决方法 1.查询活动会话 SQL> SELECT sid, serial#, username, programFROM v$sessionWHERE username 你要删除的u…

微信小程序bindgetphonenumber获取手机号阻止冒泡触发

问题&#xff1a;点击手机号弹出微信的手机号验证组件&#xff0c;这是可以的。但是我点击车牌号&#xff0c;也弹出来了&#xff0c;这就郁闷了。 以下是解决方法 点击手机号时&#xff0c;弹出选择手机号 解决&#xff1a; <view style"display: flex;justify-conte…

pdf处理命令合集

安装weasyprint用于生成pdf 单个文件合成多个pdf linux - Merge / convert multiple PDF files into one PDF - Stack Overflow

除了诺贝尔奖的红利,Pasqal 还有哪些实力?

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨浪味仙 排版丨沛贤 深度好文&#xff1a;3700字丨13分钟阅读 摘要&#xff1a;与超导量子比特相比&#xff0c;中性原子量子技术的投资成本相对较低、中性原子量子比特无需布线、还能将单…

查看Linux端口占用和开启端口命令

查看端口的使用的情况 lsof 命令 比如查看80端口的使用的情况 lsof -i tcp:80列出所有的端口 netstat -ntlp查看端口的状态 /etc/init.d/iptables status开启端口以开启端口80为例。 1 用命令开启端口 iptables -I INPUT -p tcp --dport 80 -j accpet --写入要开放的端口/…

23 - 每位教师所教授的科目种类的数量(高频 SQL 50 题基础版)

23 - 每位教师所教授的科目种类的数量 考点&#xff1a; 排序和分组 selectteacher_id,count(distinct subject_id) cnt fromTeacher group byteacher_id;