Go语言中有着两个很容易混淆的概念: 数组Array
和切片Slice
。本篇文章将就这两个数据结构的相似与区别进行分析。
Array
Go语言中的Array即为数据的一种集合,需要在声明时指定容量和初值,且一旦声明就长度固定了,访问时按照索引进行访问。通过内置函数len
可以获取数组中的元素个数。
初始化
数组在初始化时必须指定大小和初值,不过Go语言为我们提供了一些更为灵活的方式进行初始化。例如:使用...
来自动获取长度;未指定值时,用0赋予初值;指定指定元素的初值等。下面给出一些数组初始化的方式示例。
var arr [5]int //声明了一个大小为5的数组,默认初始化值为[0,0,0,0,0]
arr := [5]int{1} //声明并初始化了一个大小为5的数组的第一个元素,初始化后值为[1,0,0,0,0]
arr := [...]int{1,2,3} //通过...自动获取数组长度,根据初始化的值的数量将大小初始化为3,初始化后值为[1,2,3]
arr := [...]int{4:1} //指定序号为4的元素的值为1,通过...自动获取长度为5,初始化后值为[0,0,0,0,1]
复制代码
函数参数
Go语言数组作为函数参数时,必须指定参数数组的大小,且传入的数组大小必须与指定的大小一致,数组为按值传递的,函数内对数组的值的改变不影响初始数组:
package main
import "fmt"
//PrintArray print the value of array
func PrintArray(arr [5]int) {
arr[0] = 5
fmt.Println(arr)
}
func main() {
a := [...]int{4:1}
PrintArray(a) // [5,0,0,0,1]
fmt.Println(a) // [0,0,0,0,1]
}
复制代码
Slice
切片是Go语言中极为重要的一种数据类型,可以理解为动态长度的数组(虽然实际上Slice结构内包含了一个数组),访问时可以按照数组的方式访问,也可以通过切片
操作访问。Slice有三个属性:指针、长度和容量。指针即Slice名,指向的为数组中第一个可以由Slice访问的元素;长度指当前slice中的元素个数,不能超过slice的容量;容量为slice能包含的最大元素数量,但实际上当容量不足时,会自动扩充为原来的两倍。通过内置函数len
和cap
可以获取slice的长度和容量。
初始化
Slice在初始化时需要初始化指针,长度和容量,容量未指定时将自动初始化为长度的大小。可以通过直接获取数组的引用、获取数组/slice的切片构建或是make函数初始化数组。下面给出一些slice初始化的方式示例。
s := []int{1,2,3} //通过数组的引用初始化,值为[1,2,3],长度和容量为3
arr := [5]int{1,2,3,4,5}
s := arr[0:3] //通过数组的切片初始化,值为[1,2,3],长度为3,容量为5
s := make([]int, 3) //通过make函数初始化,值为[0,0,0],长度和容量为3
s := make([]int, 3, 5) //通过make函数初始化,值为[0,0,0],长度为3,容量为5
复制代码
其中特别需要注意的是通过切片方式初始化。若是通过对slice的切片进行初始化,实际上初始化之后的结构如下图所示:
此时x的值为[2,3,5,7,11],y的值为[3,5,7],且两个slice的指针指向的是同一个数组,也即x中的元素的值的改变将会导致y中的值也一起改变。
这样的初始化方式可能会导致内存被过度占用,如只需要使用一个极大的数组中的几个元素,但是由于需要指向整个数组,所以整个数组在GC时都无法被释放,一直占用内存空间。故使用切片操作进行初始化时,最好使用
append
函数将切片出来的数据复制到一个新的slice中,从而避免内存占用陷阱。
函数参数
Go语言Slice作为函数参数传递时为按引用传递的,函数内对slice内元素的修改将导致函数外的值也发生改变,不过由于传入函数的是一个指针的副本,所以对该指针的修改不会导致原来的指针的变化(例如append函数不会改变原来的slice的值)。具体可以根据下面的代码进行理解:
package main
import "fmt"
//PrintSlice print the value of slice
func PrintSlice(s []int) {
s = append(s, 4)
s[0] = -1
fmt.Println(s)
}
func main() {
s := []int{1,2,3,4,5}
s1 := s[0:3]
fmt.Println("s:",s) //s: [1,2,3,4,5]
fmt.Println("s1:",s1) //s1: [1,2,3]
PrintSlice(s1) //[-1,2,3,4]
fmt.Println("s:",s) //[-1,2,3,4,5]
fmt.Println("s1:",s1) //[-1,2,3]
}
复制代码
总结
- 数组长度不能改变,初始化后长度就是固定的;切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
- 结构不同,数组是一串固定数据,切片描述的是截取数组的一部分数据,从概念上说是一个结构体。
- 初始化方式不同,如上。另外在声明时的时候:声明数组时,方括号内写明了数组的长度或使用
...
自动计算长度,而声明slice
时,方括号内没有任何字符。 - unsafe.sizeof的取值不同,unsafe.sizeof(slice)返回的大小是切片的描述符,不管slice里的元素有多少,返回的数据都是24字节。unsafe.sizeof(arr)的值是在随着arr的元素的个数的增加而增加,是数组所存储的数据内存的大小。
- 函数调用时的传递方式不同,数组按值传递,slice按引用传递。