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
是由数组 arr1
从 start
索引到 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
组成的切片可以这么生成:
s := [3]int{1,2,3}[:]
// 甚至更简单的:
s := []int{1,2,3}
用切片组成的切片:
s2 := s[:]
s1
与 s
拥有相同的元素,指向相同的相关数组。
一个切片 s
扩展到大小上限:s = s[:cap(s)]
,如果再扩大的话就会导致运行时异常。
对于切片(包括 string),以下状态总是成立的:
// i是一个整数且: 0 <= i <= len(s)
s == s[:i] + s[i:]
len(s) <= cap(s)
切片也可以用类似数组的方式初始化:var x = []int{2, 3, 5, 7, 11}
。这样就创建了一个长度为 5
的数组并且创建了一个相关切片。
package main
import "fmt"
func main() {
var arr1 [6]int
// item at index 5 not included!
var slice1 []int = arr1[2:5]
// load the array with integers: 0,1,2,3,4,5
for i := 0; i < len(arr1); i++ {
arr1[i] = i
}
// print the slice
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
fmt.Printf("The length of arr1 is %d\n", len(arr1))
fmt.Printf("The length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
// grow the slice
slice1 = slice1[0:4]
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
fmt.Printf("The length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
// grow the slice beyond capacity
//slice1 = slice1[0:7 ] // panic: runtime error: slice bound out of range
}
输出:
Slice at 0 is 2
Slice at 1 is 3
Slice at 2 is 4
The length of arr1 is 6
The length of slice1 is 3
The capacity of slice1 is 4
Slice at 0 is 2
Slice at 1 is 3
Slice at 2 is 4
Slice at 3 is 5
The length of slice1 is 4
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[:] 分别是什么?
将切片传递给函数
如果你有一个函数需要对数组做操作,建议把参数声明为切片。当你调用该函数时,把数组分片,创建为一个切片
引用并传递给该函数。
func sum(a []int) int {
s := 0
for i := 0; i < len(a); i++ {
s += a[i]
}
return s
}
func main() {
var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])
}
使用 make() 创建切片
当未定义相关数组时,可以使用 make()
函数来创建切片,同时创建相关数组。
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
个值,那么只需要:
slice1 := make([]type, len, cap)。
make
的使用方式是:func make([]T, len, cap)
,其中 cap
是可选参数,表示数组的长度。
所以下面两种方法可以生成相同的切片:
make([]int, 50, 100)
new([100]int)[0:50]
下面的示例描述了使用 make
方法生成的切片的内存结构:
package main
import "fmt"
func main() {
var slice1 []int = make([]int, 10)
// load the array/slice:
for i := 0; i < len(slice1); i++ {
slice1[i] = 5 * i
}
// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
fmt.Printf("\nThe length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
}
输出:
Slice at 0 is 0
Slice at 1 is 5
Slice at 2 is 10
Slice at 3 is 15
Slice at 4 is 20
Slice at 5 is 25
Slice at 6 is 30
Slice at 7 is 35
Slice at 8 is 40
Slice at 9 is 45
The length of slice1 is 10
The capacity of slice1 is 10
因为字符串是纯粹不可变的字节数组,它们也可以被切分成 切片
。
new() 和 make() 的区别
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
- new(T):为每个新的类型
T
分配一片内存,初始化为0
并且返回类型为*T
的内存地址,这种方法返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体,它相当于 &T{}。 - make(T):返回一个类型为
T
的初始值,它只适用于3种内置的引用类型:切片
、map
和channel
。
换言之,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 现在的值又分别是多少?
如何理解new
、make
、slice
、map
、channel
的关系?
slice
、map
以及channel
都是golang
内置的一种引用类型,三者在内存中存在多个组成部分, 需要对内存组成部分初始化后才能使用,而make
就是对三者进行初始化的一种操作方式。new
获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作, 所以slice
、map
、channel
需要make
进行初始化并获取对应的内存地址,而非new
简单的获取内存地址。
多维切片
和数组一样,切片通常也是一维的,但是也可以由一维组合成多维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配(通过 make
函数)。
多维切片申明格式:var slice1 [][]...[]type
其中,slice1
为切片的名字,type
为切片的类型,每个[ ]
代表着一个维度,切片有几个维度就需要几个[ ]
。
下面以二维切片为例,声明一个二维切片并赋值:
// 声明一个二维切片
var slice [][]int
// 为二维切片赋值
slice = [][]int{{10}, {100, 200}}
上面的代码也可以简写为下面这样:
// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}
切片重组(reslice)
我们已经知道切片创建的时候通常比相关数组小,例如:
slice1 := make([]type, start_length, capacity)
其中 start_length
作为切片初始长度而 capacity
作为相关数组的长度。
这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing
,做法如下:slice1 = slice1[0:end]
,其中 end
是新的末尾索引(即长度)。
将切片扩展 1 位可以这么做:
sl = sl[0:len(sl)+1]
切片可以反复扩展直到占据整个相关数组。
package main
import "fmt"
func main() {
slice1 := make([]int, 0, 10)
// load the slice, cap(slice1) is 10:
for i := 0; i < cap(slice1); i++ {
slice1 = slice1[0:i+1]
slice1[i] = i
fmt.Printf("The length of slice is %d\n", len(slice1))
}
// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
}
输出结果:
The length of slice is 1
The length of slice is 2
The length of slice is 3
The length of slice is 4
The length of slice is 5
The length of slice is 6
The length of slice is 7
The length of slice is 8
The length of slice is 9
The length of slice is 10
Slice at 0 is 0
Slice at 1 is 1
Slice at 2 is 2
Slice at 3 is 3
Slice at 4 is 4
Slice at 5 is 5
Slice at 6 is 6
Slice at 7 is 7
Slice at 8 is 8
Slice at 9 is 9
切片的复制与追加
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的示例描述了从拷贝切片的 copy
函数和向切片追加新元素的 append
函数。
package main
import "fmt"
func main() {
slFrom := []int{1, 2, 3}
slTo := make([]int, 10)
n := copy(slTo, slFrom)
fmt.Println(slTo)
fmt.Printf("Copied %d elements\n", n) // n == 3
sl3 := []int{1, 2, 3}
sl3 = append(sl3, 4, 5, 6)
fmt.Println(sl3)
}
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
的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 src
和 dst
的长度最小值。如果 src
是字符串那么元素类型就是 byte
。如果你还想继续使用 src
,可以在拷贝结束后执行 src = dst
。