一、 数组与切片
在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块(注:与python不同的是,定义一个数组后,此数组里元素只能是一种数据类型,且其占用的内存是连续分配的,既然数组的每个元素类型相同,又是连续分配,就可以以固定速度索引数组中的任意数据,速度非常快,会比python列表的索引取值快);切片时对底层数组进行了抽象,是围绕动态数组的概念构建的,可以按需自动增长和缩小,切片的动态增长是通过内置函数 append 来实现的。
- 定义及初始化
数组:
如下,对于数组有如下四种主要的定义及初始化方式,如前所述,数组是是有固定长度的,所以在定义或者初始化时便确定了数组的长度,数组的长度不会再发生改变,也就是任何试图超出数组长度的操作都是不合法的。
切片:var array1 [5]int fmt.Println("方式一:",array1) //[0 0 0 0 0] array2 := [5]int{10, 20, 30, 40, 50} fmt.Println("方式二:",array2) //[10 20 30 40 50] array3 := [...]int{10, 20, 30, 40, 50} fmt.Println("方式三:",array3) //[10 20 30 40 50](初始数据个数为数组的长度) array4 := [5]int{1: 10, 2: 20} fmt.Println("方式四:",array4) //[0 10 20 0 0]
注意,如上声名的切片并非空切片,通过打印结果即可看出来。如果通过append方法添加元素时候,会在上述默认切片后面追加元素。如下才是声名空切片的方式://指定长度但不指定容量时,容量和长度一致 slice1 := make([]int, 5) fmt.Println("slice1:", slice1) // [0 0 0 0 0] //指定长度为5,容量为8的切片 slice2 := make([]int, 5, 8) // [0 0 0 0 0] fmt.Println("slice2:", slice2) //通过字面量声明切片,长度与容量均为3 slice3 := []string {"red", "yellow", "green"} fmt.Println("slice3:", slice3) //[red yellow green] //使用索引声明切片 slice4 := []string {9:"abc"} fmt.Println("slice4:", slice4) //[ abc] fmt.Printf("slice4 length is %d, cap is %d", len(slice4), cap(slice4)) //slice4 length is 10, cap is 10
var slice5 []int slice6 := make([]int, 0) slice7 := []int {} fmt.Println(slice5, slice6, slice7) //[] [] []
- 函数调用方式
数组:
如下,每次函数 foo 被调用时,必须在栈上分配 8 MB 的内存。之后,整个数组的值(8 MB 的内存)被复制到刚分配的内存里。 虽然 Go 语言自己会处理这个复制操作,不过还有一种更好且更有效的方法来处理这个操作。可以只传入指向数组的指针,这样只需要复制 8 字节的数据而不是8 MB 的内存数据到栈上。
函数 foo 接受一个指向 100 万个整型值的数组的指针。现在将数组的地址传入函数,只需要在栈上分配 8 字节的内存给指针就可以。这个操作会更有效地利用内存,性能也更好。不过要意识到,因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存。// 声明一个需要 8 MB 的数组 var array [1e6]int // 将数组传递给函数 foo foo(array) // 函数 foo 接受一个 100 万个整型值的数组 func foo(array [1e6]int) { } /// // 分配一个需要 8 MB 的数组 var array [1e6]int // 将数组的地址传递给函数 foo foo(&array) // 函数 foo 接受一个指向 100 万个整型值的数组的指针 func foo(array *[1e6]int) { }
二、切片的使用
- 赋值和切片
通过如上方式我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分。对底层数组容量是 k 的切片 slice[i:j]来说,长度: j - i,容量: k - i。slice := []int{10, 20, 30, 40, 50} //长度与容量均为5 newSlice := slice[1:3] //长度为2,容量为4
如上图展示了实例两个切片共享同一个底层数组的情况,需要记住的是,现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到。
把 35 赋值给 newSlice 的第二个元素(索引为 1 的元素)的同时也是在修改原来的 slice的第 3 个元素(索引为 2 的元素)。newSlice[1] = 35
切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行时异常,如上操作会报错。与切片的容量相关联的元素只能用于增长切片。在使用这部分元素前,必须将其合并到切片的长度里。(append可以做到)newSlice[3] = 45 //报错
- 切片增长
Go 语言内置的 append函数会处理增加长度时的所有操作细节,函数 append 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。
如上操作后, 底层数组的布局图如下:// 创建一个整型切片,其长度和容量都是 5 个元素 slice := []int{10, 20, 30, 40, 50} // 创建一个新切片, 其长度为 2 个元素,容量为 4 个元素 newSlice := slice[1:3] // 使用原有的容量来分配一个新元素,将新元素赋值为 60 newSlice = append(newSlice, 60)
因为 newSlice 在底层数组里还有额外的容量可用,append 操作将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的 slice 共享同一个底层数组,slice 中索引为 3 的元素的值也被改动了。如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值。
如上操作后,newSlice 拥有一个全新的底层数组,这个数组的容量是原来的两倍,布局图如下:slice := []int{10, 20, 30, 40} newSlice := append(slice, 50)
函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量。