Go 语言笔记 - 数组
有经验的程序员都知道,实际项目中很难遇到不需要存储和读取集合数据的情况。不论是读写数据库或文件,或者访问网络,都需要有一种方式来处理接收和发送的数据。
Go 语言中有 3 种数据结构可以让用户管理集合数据:数组、切片(slice)和映射(map)。数组是切片和映射的基础数据结构
数组
- 数组创建后占用一块连续的内存
- 通过索引(也称下标)访问数组元素
- 数组声明后长度和值的类型就无法修改
语法
- 创建数组
// 声明格式 var 变量名 [长度]类型
var arr1 [5]int // 初始化为 [0,0,0,0,0]
var arr2 = [...]int{1, 2, 3} // 初始化为 [1, 2, 3]
arr3 := [5]int{1,2,3,4,5} // 初始化为 [1, 2, 3, 4, 5]
- 修改数组元素
通过索引下标获取和修改数组元素
arr1[0] = 1 // arr1 [1, 0, 0, 0, 0]
arr1[4] = 5 // arr1 [1, 0, 0, 0, 5]
arr1[5] = 6 // invalid array index 5 (out of bounds for 5-element array)
- 获取长度和容量
数组在初始化时,会指定长度,这个值可以通过len()
函数获得。数组容量可以通过cap()
函数获得。数组创建后长度和容量是一致的。
len(arr1)
cap(arr1)
4.遍历
使用 for
和 range
遍历数组
arr := [5]int{1,2,3,4,5}
for index, value := range arr {
fmt.Println("index: ", index, " value: ", value)
}
数组的内部实现
在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同类型的元素的连续块。数组存储的类型可以是内置类型,如整型或字符串,也可以是某种结构类型。
在图 1-1 中可以看到数组的表示。灰色格子代表数组里的元素,每个元素都是紧邻另一个元素,每个元素包含相同的类型,这个例子里是整数,并且每个元素拥有一个唯一的索引(也称下标)来访问。
图 1-1 数组的存储结构数组是一个长度固定的数据类型,因为其占用的内存是连续分配的,CPU 能把正在使用的数据缓存更长时间。而且内存连续很容易计算索引,可以快速迭代数组里的所有元素。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。既然数组的每个元素类型相同,又是连续分配,就可以以固定速度索引数组中的任意数据,速度非常快。
总结
数组的特点:
- 数组创建后占用一块连续的内存
- 通过索引(也称下标)访问数组元素
- 数组声明后长度和值的类型就无法修改
内存连续的好处:
- CPU 能把正在使用的数据缓存更长时间
- 容易计算索引
- 快速迭代数组里的所有元素
类型固定的好处:
- 每次访问一个元素时在内存中移动的距离固定
推论:内存连续分配,又类型固定,就可以以固定的速度索引数组中的任意数据,速度非常快。
在函数间传递数组
根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组都会被完整赋值,并传递给函数。
为了验证这个操作,我们来创建一个包含 100 万个 int 类型元素的数组。在 64 位架构的机器上,这需要 800 万字节,即 8 MB 的内存。
var array [1e6]int
func main() {
foo(array)
}
func foo(array [1e6]int) {
fmt.Println("array length: ", len(array))
}
每次函数 foo 被调用时,都要在栈上分配 8MB 的内存。之后,整个数组的值(8MB的内存)被复制到刚分配的内存里。因此这个操作的代价是随着数组的长度而变大。我们可以通过传入数组的指针的方式来处理这个操作。这样只需要复制 8 字节数据而不是 8 MB 的数据到栈上。
var array [1e6]int
func main() {
foo(&array)
}
func foo(array *[1e6]int) {
fmt.Println("array length: ", len(array))
}
这个操作会更有效地利用内存,性能也更好。不过因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存。使用切片能更好地处理这类共享问题。