切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append
来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的。所以切片还能获得索引、迭代以及垃圾回收优化的好处。
内部实现
切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有3个字段的数据结构,这些数据结构包含Go
语言需要操作底层数据的元数据。这3个字段分别是指向底层数据组的指针,切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。
创建和初始化
Go
语言中有几种方法可以创建和初始化切片,知道切片所需的容量通常会决定要如何创建切片。
make和切片字面量
一种创建切片的方法是使用内置的make
函数。当使用make
时,需要传入一个参数,指定切片的长度,如:
//使用长度声明一个字符串切片
//创建一个字符串切片,其长度和容量都是5个元素
slice := make([]string, 5)
上面的例子只指定了长度,那么切片的容量和长度相等。也可以分别指定长度和容量,如:
//使用长度和容量声明整型切片
//创建一个整型切片。长度为3个元素,容量为5个元素
slice := make([]int, 3, 5)
分别指定长度和容量时,创建的切片,底层数组的长度时指定的容量,但是初始化后并不能访问所有的数组元素。拿上述代码为例,声明了一个长度为3,容量为5的切片,如果我执行slice[3] = 40
,那么就会报错panic: runtime error: index out of range
。也就是说这个切片可以访问3个元素,而底层数组拥有5个元素。剩余的2个元素可以在后期操作中合并到切片,可以通过切片访问这些元素。如果基于这个切片创建新的切片,新切片会和原有切片共享数组,也能通过后期操作访问剩下的元素。
不允许创建容量小于长度的切片。 另一种常用的创建切片的方法时使用切片字面量,这种方法和创建数组类似,只是不需要指定
[]
运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定。如:
//通过切片字面量来声明切片
//创建字符串切片,其长度和容量都是5个元素
slice := []string{"red", "blue", "green", "yellow", "pink"}
//创建一个整型切片,其长度和容量都是3个元素
slice := []int{10, 20, 30}
当使用切片字面量时,可以设置初始长度和容量。在初始化时给出所需的长度和容量作为索引。如:
//使用索引声明切片
//创建字符串切片,使用空字符串初始化第100个元素
slice := []string{99: ""}
nil和空切片
有时,程序可能需要声明一个值为nil
的切片。只要在声明时不做任何初始化,就会创建一个nil
切片,如:
//创建nil切片
//创建nil整型切片
var slice []int
nil
切片是常见的创建切片的方法。可以用于很多标准库和内置函数。在需要描述一个不存在的切片时,nil
切片会很好用,例如,函数要求返回一个切片但是发生异常的时候
利用初始化,通过声明一个切片可以创建一个空切片,如:
//声明空切片
//使用make创建空的整型切片
slice := make([]int, 0
slice := []int{}
空切片在底层数组包含0个元素,也没有分配任何存储空间。想表示空集合时空切片很有用,例如,数据库查询返回0个结果。
使用切片
赋值和切片
对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一致。如:
//使用切片字面量来声明切片
//创建一个整型切片,其长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}
//改变索引为1的元素的值
slice[1] = 25
切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分,如:
//使用切片创建切片
//创建一个整型切片,其长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}
//创建一个新切片,其长度为2个元素,容量为4个元素
newSlice := slice[1:3]
第一个切片slice能够看到底层数组全部5个元素的容量,不过之后的newSlice就看不到,对于newSlice,底层数组的容量只有4个元素。newSlice无法访问到它所指向的底层数组的第一个元素之前的部分。所以对newSlice来说,之前的那些元素就是不存在的。
//如何计算长度和容量
对底层数组容量是k的切片slice[i:j]来说
长度: j - i
容量:k - i
现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到,如:
//修改切片内容可能导致的结果
//创建一个整型切片,长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}
//创建一个新切片,其长度是2个元素,容量是4个元素
newSlice := slice[1:3]
//修改newSlice索引为1的元素,同时也修改了原来slice的索引为2的元素
newSlice[1] = 35
切片只能访问到长度内的元素。试图访问超出其长度的元素将会导致异常,与切片的容量相关联的元素只能用于增长切片。在使用这部分元素前,必须将其合并到切片的长度里。
切片增长
相对于数组而言,切片可以按需增加容量,go
语言内置的append
函数会处理增加长度的所有操作细节。要使用append
,需要一个被操作的切片和一个要追加的值,返回一个包含修改结果的新切片。append
总是会增加新切片的长度,而容量有可能会改变,也有可能不会改变,这取决于被操作切片的可用容量。
//使用append向切片增加元素
//创建一个整型切皮,其长度和容量都是5个元素
slice := []int{10, 20, 30, 40, 50}
//创建一个新切片,其长度为2个元素,容量为4个元素
newSlice := slice[1:3]
//使用原来的容量来分配一个新元素
newSlice = append(newSlice, 60)
因为newSlice在底层数组里还有额外的容量可用,append
操作将可用的元素合并到切片的长度,并对其进行赋值。
由于和原始的slice共享同一个底层数组,slice中索引为3的元素也被改动了,也就是{10, 20, 30, 60, 50}
如果切片的底层数组没有足够的可用容量,append
函数会创建一个新的底层数组。并且将原来的底层数组的值复制到新数组里,再追加新的值,如:
//使用append同时增加切片的长度和容量
package main
import "fmt"
func main() {
slice := []int{10, 20, 30, 40, 50}
newSlice := append(slice, 60)
slice[2] = 35
for i := 0; i < len(slice); i++ {
fmt.Printf("slice %d is %d \n", i, slice[i])
}
for i := 0; i < len(newSlice); i++ {
fmt.Printf("newSlice %d is %d \n", i, newSlice[i])
}
fmt.Printf("new capacity is %d, newSlice capacity is %d", cap(slice), cap(newSlice))
// slice 0 is 10
// slice 1 is 20
// slice 2 is 35
// slice 3 is 40
// slice 4 is 50
// newSlice 0 is 10
// newSlice 1 is 20
// newSlice 2 is 30
// newSlice 3 is 40
// newSlice 4 is 50
// newSlice 5 is 60
// new capacity is 5, newSlice capacity is 10
}
当append
操作完成后,newSlice拥有一个全新的底层数组,,这个数组的容量是原来的两倍。函数append
会智能地处理底层数组的容量增长,在切片的容量小于1000个元素时,总是会成倍地增长容量,一旦元素个数超过1000,容量地增长因子会设为1.25,也就是会每次增加25%地容量。
学习了append
之后,就可以尝试练习一些算法,leetcode上的基础算法 Two Sum
创建切片时的3个索引
在创建切片时还可以使用之前我们没有提及的第三个索引选项。第三个索引可以用来控制新切片的容量。起目的并不是要增加容量,而是要限制容量。如:
//使用切片字面量声明一个字符串切片,长度和容量都是5个元素
source := []string{"php", "java", "python", "c", "javascript"}
//使用2个索引来创建切片
slice := source[2:3]
//使用3个索引创建切片
newSlice := source[2:3:4]
fmt.Printf("source capacity is %d, slice capacity is %d, newSlice capacity is %d", cap(source), cap(slice), cap(newSlice)) //source capacity is 5, slice capacity is 3, newSlice capacity is 2
计算长度和容量: 对于 slice[i:j:k], 长度:j - i 容量:k - i 所以上述newSlice切片,开始元素的索引位置是2,期望长度是1 所以
2 + 1
, 期望容量是2,所以2 + 2
如果试图设置的容量比可用的容量还大,就会得到一个runtime error: out of range
.
这样做的好处就是可以避免一些因为共享底层数组导致的一些问题。我们说过内置函数append
会首先使用可用容量。一旦没有可用容量,会分配一个新的底层数组。如果我们没有定义新切片的容量,很有可能容量大于长度,那么我们对新切片进行append
的时候,很容易忘记了新的切片还和原有的切片共享底层数组,从而影响了原有切片。所以在创建新切片的时候,设置长度和容量保持相同可以避免这种情况。
内置函数append
也是一个可变参数的函数。可以一次追加多个值。如果使用...运算符。可以将一个切片的所有元素追加到另一个切片里。如:
//将一个切片追加到另一个切片
// 创建两个切片
s1 := []int{1, 2}
s2 := []int{3, 4}
//将两个切片追加在一起, 打印
fmt.Printf("%v\n", append(s1, s2...)) //[1 2 3 4]
迭代切片
切片是一个集合,可以迭代其中的元素。关键字range
,可以配合关键字for
来迭代切片里的元素,如:
// 使用for range迭代切片
// 创建一个整形切片,长度和容量都是4
slice := []int{10, 20, 30, 40}
// 迭代元素并显示
for index, value := range slice {
fmt.Printf("index : %d value: %d\n", index, value)
}
// index : 0 value: 10
// index : 1 value: 20
// index : 2 value: 30
// index : 3 value: 40
当迭代切片时,关键字range
会返回两个值。第一个值是当前迭代到的索引值,第二个值是该位置对应元素值得一个副本。这个副本不是返回对该元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。如:
/ 使用for range迭代切片
// 创建一个整形切片,长度和容量都是4
slice := []int{10, 20, 30, 40}
// 迭代元素并显示
for index, value := range slice {
fmt.Printf("value : %d value-addr: %X elem-addr: %X\n", value, &value, &slice[index])
}
// value : 10 value-addr: C420012078 elem-addr: C4200100C0
// value : 20 value-addr: C420012078 elem-addr: C4200100C8
// value : 30 value-addr: C420012078 elem-addr: C4200100D0
// value : 40 value-addr: C420012078 elem-addr: C4200100D8
因为迭代返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以value的地址总是相同的。要想获取每个元素的地址,可以使用切片变量和索引值。 如果不需要索引值,可以使用站位字符来忽略这个值,如:
for _, value := range slice {
fmt.Printf("value: %d\n", value)
}
// value : 10
// value : 20
// value : 30
// value : 40
关键字range
总是会从切片头部开始迭代,如果想对迭代做更多的控制,可以使用传统的for循环,如:
// 使用传统for循环对切片进行迭代
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index :=2; index < len(slice); index++ {
fmt.Printf("slice %d is %d\n", index, slice[index])
}
// slice 2 is 30
// slice 3 is 40
有两个特殊的内置函数len
和cap
,可以用于处理数组、切片和通道。对于切片,函数len
返回切片的长度,函数cap
返回切片的容量。
多维切片
// 声明多维切片
slice := [][]int{{10}, {100, 200, 300}}
fmt.Printf("slice capacity is %d\nslice[1] capacity is %d\n", cap(slice), cap(slice[1]))
// slice capacity is 2
// slice[1] capacity is 3
在函数间传递切片
在64位架构的机器上,一切切片需要24个字节的内存:指针字段需要8个字节,长度和容量分别需要8个字节。由于切片的尺寸很小,在函数件复制和传递切片成本也很低,举两个例子可以看出在函数件传递的效果如:
// 在函数间传递切片
func main()
{
slice := []int{10, 20, 30}
foo(slice)
for i := 0; i < len(slice); i++ {
fmt.Printf("slice %d is %d\n", i, slice[i])
}
}
func foo(slice []int) []int
{
slice[0] = 15
return slice
}
// slice 0 is 15
// slice 1 is 20
// slice 2 is 30
func main (){
slice := []int{10, 20, 30}
newSlice := foo(slice)
for i := 0; i < len(slice); i++ {
fmt.Printf("slice %d is %d\n", i, slice[i])
}
for i := 0; i < len(newSlice); i++ {
fmt.Printf("newSlice %d is %d\n", i, newSlice[i])
}
}
func foo(slice []int) []int{
slice = append(slice, 40)
slice[0] = 15
return slice
}
// slice 0 is 10
// slice 1 is 20
// slice 2 is 30
// newSlice 0 is 15
// newSlice 1 is 20
// newSlice 2 is 30
// newSlice 3 is 40