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 mainimport "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,5for i := 0; i < len(arr1); i++ {arr1[i] = i}// print the slicefor 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 sliceslice1 = 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 2Slice at 1 is 3Slice at 2 is 4The length of arr1 is 6The length of slice1 is 3The capacity of slice1 is 4Slice at 0 is 2Slice at 1 is 3Slice at 2 is 4Slice at 3 is 5The length of slice1 is 4The 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 := 0for 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 mainimport "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 0Slice at 1 is 5Slice at 2 is 10Slice at 3 is 15Slice at 4 is 20Slice at 5 is 25Slice at 6 is 30Slice at 7 is 35Slice at 8 is 40Slice at 9 is 45The length of slice1 is 10The 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 mainimport "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] = ifmt.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 1The length of slice is 2The length of slice is 3The length of slice is 4The length of slice is 5The length of slice is 6The length of slice is 7The length of slice is 8The length of slice is 9The length of slice is 10Slice at 0 is 0Slice at 1 is 1Slice at 2 is 2Slice at 3 is 3Slice at 4 is 4Slice at 5 is 5Slice at 6 is 6Slice at 7 is 7Slice at 8 is 8Slice at 9 is 9
切片的复制与追加
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的示例描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。
package mainimport "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 == 3sl3 := []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。