切片是一种数据结构,更便于使用和管理数据集合。切片才是实际开发中最多使用的,它是围绕着动态数组的概念构建,可以按需自动增长和缩小。
切片的内部实现和基础功能
- 切片是一种数据结构
- 切片可以按需动态增长和缩小
- 切片的底层内存也是连续块分配,能享受及使用索引,迭代,垃圾回收
内部实现
切片是一个很小的对象,对底层数组进行了抽象,它是有3个字段的数据结构。
- 指向底层数组的指针
- 长度:切片访问的元素个数(长度)
- 容量:切片允许增长的元素个数(长度)
创建切片和初始化
Go语言在创建切片有好几种方法。
是否知道切片需要的容量会决定要如何创建切片。
make关键字创建切片
内置的make函数,当使用make的时候 ,需要传入一个参数,指定切片的长度。
// 创建一个字符串切片
// 只指定了长度,长度和容量都是5
slice := make([]string, 5)
只有指定长度,没有指定容量,那么切片的容量和长度相同。
也可以分别指定长度和容量。
// 指定了长度是3,容量是5
slice := make([]string,3,5)
// 设置访问索引3
slice[3] = "apple"
// 索引越界
// panic: runtime error: index out of range [3] with length 3
这里分别指定了长度是3,容量是5的字符串切片。
不允许创建容量小于长度的切片。
底层的数组的长度是指定容量(这里底层数组是5的长度),但我们声明了切片长度是3,我们也只能访问3以内的索引,并不能访问3,4的索引。
剩余的2个元素可以在后期操作中合并切片,可以通过切片访问这些元素。
如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多作容量的元素。
切片字面量创建切片
通过切片字面量创建切片。
// 创建字符串切片
// 创建长度和容量都是2,根据初始化的值确定
slice1 := []string{"apple", "huawei"}
也可以指定长度和容量创建
// 创建长度和容量都是100的字符串切片
slice2 := []string{1:"我是第2个元素",99: "我是第100个元素,其它元素都是空值"}
fmt.Println(len(slice2)) //100
使用索引声明切片
slice3 := []string{99: ""}
fmt.Println(len(slice3)) // 100
数组和切片的不同
声明方式不同,数组必须指定长度,切片无需指定长度
// 创建int类型长度为3的数组
array1 := [3]int{10, 20, 30}
// 创建int类型长度为3,容量为3的切片
slice1 := []int{10, 20, 30}
fmt.Println(array1) // [10 20 30]
fmt.Println(slice1) // [10 20 30]
数组和切片的底层数组结构上是致,数组的长度一经指定不能改变,但切片可以动态的改变数组底层长度。
nil和空切片
声明时不做任何初始化,就等同于创建了一个nil切片。
nil切片
描述一个不存在的切片
// 创建nil整型切片
var slice []int
空切片
空切片在底层数组包含0个元素,没有分配任何空间。空集合
// make创建长度为0的空切片
var slice2 = make([]int, 0)
// 字面量方式 创建
slice3 := []int{}
无论是nil切片还是空切片,都可以使用内置函数,append,len和cap。
使用切片
赋值和切片
访问和赋值
切片的赋值和访问与数组完全一样。
// 声明一个int类型的切片,初始长度为8,容量为5
slice := []int{10, 20, 30, 40, 50}
fmt.Println(slice[1]) // 20
// 改变索引1的值为200
slice[1] = 200
fmt.Println(slice[1]) // 200
使用切片创建切片
创建切片的语法:[ start, end, cap ]
- start 开始索引
- end 结束索引
- cap 指定容量
// 创建一个整型切片,长度和容量都是5
slice := []int{10, 20, 30, 40, 50}
// 从slice切片中创建新切片
// 长度是2,
newSlice := slice[1:3]
slice切片能看到底层数组的5个元素,而newSlice切片则不行。
newSlice底层的数组容量只有4个元素。
切片创建的切片会和源切片共享同一个底层数组。如果一个切片修改了底层数组的共享部分,另一个切片也同样生效。
更多示例:
如何计算长度和容量
假设底层数组容量是K,那么计算slice[ i:j ]方法如下:
- 长度: j - i, 这里的j是指新切片中的end值
- 容量: k - i,这里的k是指源切片中的容量值
示例
// 创建一个整型切片,长度和容量都是5
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
newSlice := slice[ 1: 3]
// 计算方法 k = slice切片的容量 5
// newSlice的长度: 3 - 1 = 2
// newSlice的容量: 5 - 1 = 4
切片增长
append
先介绍append的使用,接下来再描述增长动态扩容的细节
要使用append,需要一个被操作的切片和一个要增加的值。
示例:
// 声明一个int类型的切片,长度为5,容量为5
slice := []int{10, 20, 30, 40, 50}
newSlice := append(slice, 60)
// 这里newSlice的长度是6容量是10
debug调试能看到内存中的长度和容量
append操作成功后一定会增加新切片的长度,但容量是动态的有可能会有变化也有可能不会有变化,取决于被操作的切片的可用容量。
如上示例,slice的切片长度是5,容量也是5,这里新加一个元素,长度变成6了,那么容量必须也动态扩容,当前Go语言中容量扩容会在上一个容量的基础上乘以2,这里newSlice的切片容量即5*2=10。同时会创建一个新的底层数组,不同于源slice底层数组。
这里如果我们继续往newSlice中添加数据,那么只是长度不超过10,那么切片容量也不会有变化,底层数组也不会有变化。
底层数组容量动态扩展的原则是:
切片容量小于1000个元素的时候 ,会成倍的增长,元素超过1000的时候 ,会1.25倍的增长。
append中的…运算符
内置函数append也是一个可变参数的函数,可以在一次调用传递多个追加的值。
如果使用…运算符,可以将一个切片的所有元素追加到另一个切片里。
示例:
slice := []string{"apple", "huawei", "oppo", "xiaomi"}
slice2 := []string{"black", "blue", "green"}
fmt.Println(append(slice2, slice...))
// 输出结果:[black blue green apple huawei oppo xiaomi]
先来看一个案例:
这里的第2个切片使用append,会不小心把第1个切片的值也改变了。
原因是:因为共享了同一个底层数组,第2个切片还有容量可以使用,于就在赋值的时候直接把值赋在了这个位置。
如何解决这个问题,可以引入第3个参数
创建切片时的3个索引(cap)
用来控制新切片的容量。目的不是为了增加容量是为了限制容量。
将长度和容量设置一样,就可以让新切片的第1个append操作创建一个新的底层组,这样就不会和源切片共享一个底层数组,改变值时也不会影响到源切片。
slice := []string{"apple", "huawei", "oppo", "xiaomi"}
// 对第三元素做切片,并限制容量
newslice := slice[2:3:3]
newslice = append(newslice, "add1")
fmt.Println(slice) // [apple huawei oppo xiaomi]
fmt.Println(newslice) // [oppo add1]
迭代切片
range关键字
range可以配置for循环来迭代切片的元素。
示例:
slice := []string{"apple", "huawei", "oppo", "xiaomi"}
for k, v := range slice {
fmt.Printf("index: %d, value: %s\n", k, v)
}
// 输出结果:
/*
index: 0, value: apple
index: 1, value: huawei
index: 2, value: oppo
index: 3, value: xiaomi
*/
迭代会返回2个值 ,第1个值是当前迭代到的索引位置,第2个是该位置对应元素值 的一份副本。
注意:
range创建了每个元素的副本,而不是直接返回对该元素的引用。
通过一个例子来看一下
slice := []string{"a1", "a2", "a3", "a4"}
for index, v := range slice {
fmt.Printf("index: %d, value: %s Value-Addr: %X , ElementAddr: %X\n", index, v, &v, &slice[index])
}
// 打印结果
/**
index: 0, value: a1 Value-Addr: C000040250 , ElementAddr: C00004A040
index: 1, value: a2 Value-Addr: C000040250 , ElementAddr: C00004A050
index: 2, value: a3 Value-Addr: C000040250 , ElementAddr: C00004A060
index: 3, value: a4 Value-Addr: C000040250 , ElementAddr: C00004A070
*/
可以看到Value-Addr的地址都是同一个,因为迭代返回的变量是一个迭代过程中产生的赋值的新变量。
要想获取每个元素的地址,可以使用切片变量和索引值。
多维切片
和数组一样,切片是一维的。类似数组一样可以组合成多维的。
略…
在函数中传递切片
细节描述:
- 在函数中传递切片就是要在函数间以值的方式传递切片。
- 切片的尺寸很小,在函数间复制和传递切片成本也很低。在64位的架构机器上,一个切片需要24个字节的内存,长度和容量字段分别需要8字节。
- 切片关联的数据包含在底层数组里面,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。
- 复制时只复制切片本身,不会涉及底层数组。