【Go入门学习】理解区分数组和切片
一、前言
学过 Go 的都知道在 Go 语言中有四种复合数据类型:数组、切片(Slice)、哈希表(Map)和结构体(Struct),而很多 Go 初学者也很容易把数组和切片弄混淆,所以要怎么把这两个数据类型分清楚呢?
二、数组
1.简介
数组是聚合类型,是一组同类型数据的集合,通过从0开始的下标索引访问元素值。在 Go 语言中,数组是值类型,这就意味着当你将一个数组赋值给另一个数组的时候,实际上是将这个数组拷贝了一份。
数组的声明语法为:
var 数组变量名 [元素数量]Type
语法说明如下所示:
- 数组变量名:数组声明及使用时的变量名;
- 元素数量:数组的元素数量,可以是一个表达式,但最终计算的结果必须是整型数值;
- Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
2.初始化
默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算。示例如下:
1 var a [3]int 2 a[0] = 1 3 b := [2]int{1, 2} 4 c := [...]int{3, 5, 7} 5 fmt.Println(a) // [1 0 0] 6 fmt.Println(b) // [1 2] 7 fmt.Println(c) // [3 5 7]
3.遍历数组
数组通过下标访问元素,可修改其元素值。数组的遍历通过 for 循环实现:
1 arr := [3]int{2, 4, 6} 2 for i := 0; i < 3; i++ { 3 fmt.Printf("%d ", arr[i]) 4 } // 2 4 6 5 fmt.Println() 6 for _, v := range arr { 7 fmt.Printf("%d ", v) 8 } // 2 4 6
三、切片
1.简介
数组的长度不可改变,在一定场合下就不太适用了,Go 语言则提供了一种可以动态扩容的数据类型–切片(Slice)。一个切片类型通常会写作 []T,其中 T 代表切片中元素的数据类型,切片的语法和数组类似,只是没有固定长度。
2.区别
切片和数组有如下区别:
1)和数组相比,切片除了有长度(len),还有容量(cap),容量指切片当前可容纳元素的最大数量。
2)数组是值类型,切片是引用类型。
值类型和引用类型有什么区别呢?在传递参数时,如果是值类型,对参数修改不会对原来的变量产生影响,但若是引用传递,对参数的修改也会影响到原始数据。示例如下:
1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 func change(a [3]int, s []int) { 8 a[0] += 1 9 s[0] += 1 10 s = append(s, 9) 11 } 12 13 func main() { 14 arr := [3]int{2, 4, 6} 15 sli := []int{3, 5, 7} 16 change(arr, sli) 17 fmt.Println(arr) // [2 4 6] 18 fmt.Println(sli) // [4 5 7] 19 }
在示例中,分别对数组 arr 和切片 sli 的第一个元素进行了+1操作,但从打印结果可以看出来只有切片的数据被修改了,而对数组的修改并没有改变原始数据。那为什么最后 sli 的结果不是 [4 5 7 9]呢?这是因为 append() 实际上是将切片 sli 复制了一份然后赋值给了 s,已经是一份新的数据了,也就不会对 sli 产生影响了。
3.初始化
切片的初始化可以通过数组来实现,也可以通过内置函数 make() 来实现,在使用 make() 方法时还可以设置切片的容量,在追加元素时,若切片的容量不足,则会按切片的长度的二倍进行扩容。示例如下:
1 arr := [5]int{1, 2, 3, 4, 5} 2 s1 := arr[2:] 3 fmt.Println(s1) // [3 4 5] 4 s2 := arr[:] 5 fmt.Println(s2) // [1 2 3 4 5] 6 s3 := make([]int, 3) 7 s3[0], s3[1], s3[2] = 2, 4, 6 8 fmt.Println(s3) // [2 4 6]
4.追加元素
在 Go 语言中有一个内置函数 append(),查看源码发现它是这么定义的:
func append(slice []Type, elems ...Type) []Type
内置的 append() 函数用于向 slice 追加元素,示例为:
1 arr := [5]int{1, 2, 3, 4, 5} 2 var sli []int 3 for _, v := range arr { 4 sli = append(sli, v) 5 } 6 fmt.Println(sli) // [1 2 3 4 5]
细心的人会发现源码中写的是 elems,这是不是就意味着可以一次添加多个元素呢?试一试:
1 var sli []int 2 sli = append(sli, 1, 2, 3) 3 fmt.Println(sli) // [1 2 3]
例子很简单,append() 使用起来也很方便,但问题是如果要添加的元素数量超过了切片的容量,又会发生什么情况呢?看下面的例子:
1 var y []int 2 for i := 0; i < 10; i++ { 3 y = append(y, i) 4 fmt.Printf("%d cap=%d %v\n", i, cap(y), y) 5 }
这几行代码的运行结果为:
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
可以发现切片的容量从1慢慢增加为2、4、8、16,也就是说在使用 append 将元素添加至切片时,如果超出了容量,将会返回一个容量二倍与当前切片的切片。
5.切片拷贝
在 Go 语言中,切片的拷贝使用内置函数 copy() 来实现,可以放心的是,切片拷贝是深拷贝,不用像 Python 中纠结深浅拷贝真的很幸福呢!只不过拷贝的时候需要确保目的切片有足够的容量,否则会拷贝。示例如下:
1 sli := []int{3, 5, 7} 2 res := make([]int, 5) 3 copy(res, sli) 4 fmt.Println(res) // [3 5 7 0 0] 5 fmt.Println(&sli[0], &res[0]) // 0xc000012340 0xc00000c3c0 6 var s []int 7 copy(s, sli) 8 fmt.Println(s) // []
这里 s 打印出来是空的,是由于 s 在初始化的时候没有分配内存空间,copy() 也不会为 s 分配空间,所以 sli 中的元素也就无法拷贝到 s 中了。