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