Go 语言切片

切片概念

切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是, 终止索引标识的项不包括在切片内

切片是可索引的,并且可以由 len() 函数获取长度。

和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度,我们可以这么理解:切片是一个 长度可变的数组

切片提供了计算容量的函数 cap() ,可以计算切片的最大长度(切片长度 + 数组除切片之外的长度)。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)

多个切片如果表示同一个数组的片段,它们可以共享数据。因此,一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的 母体

因为切片是数组的引用,所以不需要使用额外的内存,要比使用数组更有效率。

声明切片的格式: var identifier []type(不需要说明长度)

一个切片在未初始化之前默认为 nil,长度为 0。

切片初始化的格式:var slice1 []type = arr1[start:end]

上面表示 slice1 是由数组 arr1start 索引到 end-1 索引之间的元素构成的子集。所以 slice1[0] 就等于 arr1[start],看到这里就很好理解切片的涵义了。

  • arr1[0:len(arr1)]arr1[:], 都等于完整的 arr1 数组,还有一种表述方式是:slice1 = &arr1

  • arr1[2:]arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。

  • arr1[:3]arr1[0:3] 相同,都包含了从第一个到第三个元素。

如果你想去掉 slice1 的最后一个元素:slice1 = slice1[:len(slice1)-1]

一个由数字 1、2、3 组成的切片可以这么生成:

  1. s := [3]int{1,2,3}[:]
  2. // 甚至更简单的:
  3. s := []int{1,2,3}

用切片组成的切片:

  1. s2 := s[:]

s1s 拥有相同的元素,指向相同的相关数组。

一个切片 s 扩展到大小上限:s = s[:cap(s)],如果再扩大的话就会导致运行时异常。

对于切片(包括 string),以下状态总是成立的:

  1. // i是一个整数且: 0 <= i <= len(s)
  2. s == s[:i] + s[i:]
  3. len(s) <= cap(s)

切片也可以用类似数组的方式初始化:var x = []int{2, 3, 5, 7, 11}。这样就创建了一个长度为 5 的数组并且创建了一个相关切片。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var arr1 [6]int
  5. // item at index 5 not included!
  6. var slice1 []int = arr1[2:5]
  7. // load the array with integers: 0,1,2,3,4,5
  8. for i := 0; i < len(arr1); i++ {
  9. arr1[i] = i
  10. }
  11. // print the slice
  12. for i := 0; i < len(slice1); i++ {
  13. fmt.Printf("Slice at %d is %d\n", i, slice1[i])
  14. }
  15. fmt.Printf("The length of arr1 is %d\n", len(arr1))
  16. fmt.Printf("The length of slice1 is %d\n", len(slice1))
  17. fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
  18. // grow the slice
  19. slice1 = slice1[0:4]
  20. for i := 0; i < len(slice1); i++ {
  21. fmt.Printf("Slice at %d is %d\n", i, slice1[i])
  22. }
  23. fmt.Printf("The length of slice1 is %d\n", len(slice1))
  24. fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
  25. // grow the slice beyond capacity
  26. //slice1 = slice1[0:7 ] // panic: runtime error: slice bound out of range
  27. }

输出:

  1. Slice at 0 is 2
  2. Slice at 1 is 3
  3. Slice at 2 is 4
  4. The length of arr1 is 6
  5. The length of slice1 is 3
  6. The capacity of slice1 is 4
  7. Slice at 0 is 2
  8. Slice at 1 is 3
  9. Slice at 2 is 4
  10. Slice at 3 is 5
  11. The length of slice1 is 4
  12. The capacity of slice1 is 4

如果 s2 是一个切片,你可以将 s2 向后移动一位:s2 = s2[1:]。切片只能向后移动,s2 = s2[-1:] 会导致编译错误。切片不能被重新分片以获取数组的前一个元素。

注意:绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!


练习:给定切片 b:= []byte{‘g’, ‘o’, ‘l’, ‘a’, ‘n’, ‘g’},那么 b[1:4]、b[:2]、b[2:] 和 b[:] 分别是什么?

将切片传递给函数

如果你有一个函数需要对数组做操作,建议把参数声明为切片。当你调用该函数时,把数组分片,创建为一个切片 引用并传递给该函数。

  1. func sum(a []int) int {
  2. s := 0
  3. for i := 0; i < len(a); i++ {
  4. s += a[i]
  5. }
  6. return s
  7. }
  8. func main() {
  9. var arr = [5]int{0, 1, 2, 3, 4}
  10. sum(arr[:])
  11. }

使用 make() 创建切片

当未定义相关数组时,可以使用 make() 函数来创建切片,同时创建相关数组。

  1. var slice1 []type = make([]type, len)。

也可以简写为slice1 := make([]type, len),这里 len 是数组的长度,同时也是 slice 的初始长度。

定义一个切片:s2 := make([]int, 10),那么 cap(s2) == len(s2) == 10

make函数接受 2 个参数:元素的类型以及切片的元素个数。

如果你想创建一个切片 slice1,它不占用整个数组,只占用 len 个值,那么只需要:

  1. slice1 := make([]type, len, cap)。

make 的使用方式是:func make([]T, len, cap),其中 cap 是可选参数,表示数组的长度。

所以下面两种方法可以生成相同的切片:

  1. make([]int, 50, 100)
  2. new([100]int)[0:50]

下面的示例描述了使用 make 方法生成的切片的内存结构:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var slice1 []int = make([]int, 10)
  5. // load the array/slice:
  6. for i := 0; i < len(slice1); i++ {
  7. slice1[i] = 5 * i
  8. }
  9. // print the slice:
  10. for i := 0; i < len(slice1); i++ {
  11. fmt.Printf("Slice at %d is %d\n", i, slice1[i])
  12. }
  13. fmt.Printf("\nThe length of slice1 is %d\n", len(slice1))
  14. fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
  15. }

输出:

  1. Slice at 0 is 0
  2. Slice at 1 is 5
  3. Slice at 2 is 10
  4. Slice at 3 is 15
  5. Slice at 4 is 20
  6. Slice at 5 is 25
  7. Slice at 6 is 30
  8. Slice at 7 is 35
  9. Slice at 8 is 40
  10. Slice at 9 is 45
  11. The length of slice1 is 10
  12. The capacity of slice1 is 10

因为字符串是纯粹不可变的字节数组,它们也可以被切分成 切片

new() 和 make() 的区别

看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

  • new(T):为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 *T 的内存地址,这种方法返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体,它相当于 &T{}。
  • make(T):返回一个类型为 T 的初始值,它只适用于3种内置的引用类型:切片mapchannel

换言之,new 函数分配内存,make 函数初始化。


练习1:给定 s := make([]byte, 5),len(s) 和 cap(s) 分别是多少?s = s[2:4],len(s) 和 cap(s) 又分别是多少?

练习2:假设 s1 := []byte{‘p’, ‘o’, ‘e’, ‘m’} 且 s2 := s1[2:],s2 的值是多少?如果我们执行 s2[1] = ‘t’,s1 和 s2 现在的值又分别是多少?

如何理解newmakeslicemapchannel的关系?

  • slicemap 以及 channel 都是 golang 内置的一种引用类型,三者在内存中存在多个组成部分, 需要对内存组成部分初始化后才能使用,而 make 就是对三者进行初始化的一种操作方式。

  • new 获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作, 所以 slicemapchannel 需要 make 进行初始化并获取对应的内存地址,而非 new 简单的获取内存地址。

多维切片

和数组一样,切片通常也是一维的,但是也可以由一维组合成多维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配(通过 make 函数)。

多维切片申明格式:var slice1 [][]...[]type

其中,slice1 为切片的名字,type 为切片的类型,每个[ ]代表着一个维度,切片有几个维度就需要几个[ ]

下面以二维切片为例,声明一个二维切片并赋值:

  1. // 声明一个二维切片
  2. var slice [][]int
  3. // 为二维切片赋值
  4. slice = [][]int{{10}, {100, 200}}

上面的代码也可以简写为下面这样:

  1. // 声明一个二维整型切片并赋值
  2. slice := [][]int{{10}, {100, 200}}

切片重组(reslice)

我们已经知道切片创建的时候通常比相关数组小,例如:

  1. slice1 := make([]type, start_length, capacity)

其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。

这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。

将切片扩展 1 位可以这么做:

sl = sl[0:len(sl)+1]切片可以反复扩展直到占据整个相关数组。

  1. package main
  2. import "fmt"
  3. func main() {
  4. slice1 := make([]int, 0, 10)
  5. // load the slice, cap(slice1) is 10:
  6. for i := 0; i < cap(slice1); i++ {
  7. slice1 = slice1[0:i+1]
  8. slice1[i] = i
  9. fmt.Printf("The length of slice is %d\n", len(slice1))
  10. }
  11. // print the slice:
  12. for i := 0; i < len(slice1); i++ {
  13. fmt.Printf("Slice at %d is %d\n", i, slice1[i])
  14. }
  15. }

输出结果:

  1. The length of slice is 1
  2. The length of slice is 2
  3. The length of slice is 3
  4. The length of slice is 4
  5. The length of slice is 5
  6. The length of slice is 6
  7. The length of slice is 7
  8. The length of slice is 8
  9. The length of slice is 9
  10. The length of slice is 10
  11. Slice at 0 is 0
  12. Slice at 1 is 1
  13. Slice at 2 is 2
  14. Slice at 3 is 3
  15. Slice at 4 is 4
  16. Slice at 5 is 5
  17. Slice at 6 is 6
  18. Slice at 7 is 7
  19. Slice at 8 is 8
  20. Slice at 9 is 9

切片的复制与追加

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的示例描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。

  1. package main
  2. import "fmt"
  3. func main() {
  4. slFrom := []int{1, 2, 3}
  5. slTo := make([]int, 10)
  6. n := copy(slTo, slFrom)
  7. fmt.Println(slTo)
  8. fmt.Printf("Copied %d elements\n", n) // n == 3
  9. sl3 := []int{1, 2, 3}
  10. sl3 = append(sl3, 4, 5, 6)
  11. fmt.Println(sl3)
  12. }

func append(s[]T, x ...T) []T 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片,追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。 append 方法总是返回成功,除非系统内存耗尽了

如果你想将切片 y 追加到切片 x 后面,只要将第二个参数扩展成一个列表即可:x = append(x, y...)

func copy(dst, src []T) int copy 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 srcdst 的长度最小值。如果 src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,可以在拷贝结束后执行 src = dst