切片与动态数组
切片是围绕着动态数组的概念构建的,可以按需自动增长或缩小,还可以通过对切片再次切片来缩小一个切片的大小。
切片的底层实现是数组,因此可以索引、迭代以及优化垃圾回收过程。
内部实现
切片是对底层数组进行了抽象,并添加了相关操作方法。
切片是包含三个字段的数据结构:array、len和cap:
- array是指针类型,指向底层数组
- len是
int64
类型,表示切片的长度,即当前允许访问的长度 - cap是
int64
类型,表示切片的容量,即切片允许增长的最大值
创建和初始化
切片有三种创建方式:
- 使用make函数,函数的第一个参数是切片类型,第二个参数是切片长度,第三个参数是切片容量:
slice := make([]string, 3)
或slice := make([]string, 3, 5)
- 使用切片字面量:
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
- 指定某一位置的元素值:
slice := []string{99: ""}
nil切片 VS. 空切片
nil切片和空切片的长度和容量都是0,不同的是底层数组指针(array
字段):nil切片的array
字段是nil
,空切片的array
字段是一个地址指针,不过指向的是空数组。
nil切片用于描述一个不存在的切片,例如,函数要求返回一个切片但是发生异常的时候。
空切片在底层数组包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用, 例如,数据库查询返回 0 个查询结果时。
// 创建nil整型切片
var slice []int
// 创建空的整型切片
slice := make([]int, 0)
使用切片
我们可以像操作数组一样访问和修改切片元素。
slice := []int{10, 20, 30, 40, 50}
slice[1] = 100
使用切片创建切片
slice := []int{10, 20, 30, 40, 50}
// newSlice长度为2个元素,容量为4个元素
newSlice := slice[1:3]
切片之切片长度和容量的计算:
对底层数组容量为k的切片slice[i:j]
来说,切片之切片的长度是j-i
,即切片初始时可以访问的元素个数;容量是k-i
,即与该切片相关联的所有元素的数量。
切片的增长
使用append()
函数可以向切片中添加值(使得切片的长度变长,即切片中的len
字段增加),返回一个包含修改结果的新切片。
切片的增长分为两种情况:
- 增长后的切片长度不大于切片容量
- 增长后的切片长度大于切片容量
对于前者,增长前后切片共享同一底层数组:
slice := []int{10, 20, 30, 40}
newSlice := slice[1:3]
newSlice = append(newSlice, 60)
而当增长后的切片长度超出容量时,则返回的切片将会指向一个新建的底层数组,并拷贝原数组中的数据:
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)
所以,内置函数 append 会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题。对切片内容的修改会影响多个切片,却很难找到问 题的原因。
为此,可以在生成切片时将长度和容量设置为相同的值,这样后面进行
append()
操作时都会新建底层数组,而不是共享原切片的底层数组
slice := []int{10, 20, 30, 40}
newSlice := slice[2:3:3] // 设置切片长度等于容量
newSlice = append(newSlice, 50)
迭代切片
迭代切片可以使用Golang中的range
关键字和传统for
循环两种方式。
可以使用range
关键字
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Println("Index: %d; Value: %d: %d", index, value)
}
range
关键字返回一个值时,返回的是Index;返回两个值时,第一个值是Index,第二个值是Value。
需要强调的是:range
关键字返回的是每个元素的副本,而不是元素的引用:
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}
// Output :
// Value: 10 Value-Addr: C00001A0A0 ElemAddr: C0000160C0
// Value: 20 Value-Addr: C00001A0A0 ElemAddr: C0000160C8
// Value: 30 Value-Addr: C00001A0A0 ElemAddr: C0000160D0
// Value: 40 Value-Addr: C00001A0A0 ElemAddr: C0000160D8
可以看到,在for
循环中value的地址和slice中元素的地址并不相同,且value地址在迭代过程中是不变的,即循环过程中是对同一变量的重复赋值。
使用传统for
循环
slice := []int{10, 20, 30, 40}
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
有两个特殊的内置函数 len 和 cap,可以用于处理数组、切片和通道。对于切片,函数 len 返回切片的长度,函数 cap 返回切片的容量。
多维切片
和数组一样,切片是一维的。也可以像数组一样,组合多个一维切片实现多维切片。
slice := [][]int{{10}, {100, 200}}
多维切片的增长
增长多维切片的内层切片和增长一维切片的操作相同:
slice := [][]int{{10}, {100, 200}}
slice[0] = append(slice[0], 20)
在函数间传递切片
上一小节中提到在函数间传递数组的时间代价和空间代价都是巨大的,为此提出可以在函数间传递数组指针,但是这么做是极其危险的,可能导致对数组数据的非法修改且错误难以追踪。
其实,最好的方式是使用本节介绍的切片在函数间传递数组。
在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节。在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。
同时,也可以通过设置生成切片的容量来控制在调用函数中可以访问的底层数组范围和增长范围,防止错误的增长数据。