Go 学习笔记(六)数组

本文原创地址:博客园骏马金龙go 基础系列:数组

了解 Python、Perl、JavaScript 的人想必都知道它们的数组是动态的,可以随需求自动增大数组长度。但 Go 中的数组是固定长度的,数组一经声明,就无法扩大、缩减数组的长度。但 Go 中也有类似的动态 "数组",称为 slice 数据结构,在下一篇文章会详细解释它。

Go 中的数组是 slice 和 map 两种数据类型的基础,这两种数据类型的底层都是通过数组实现的。

数组的存储方式

当在 Go 中声明一个数组之后,会在内存中开辟一段 固定长度的、连续的空间 存放数组中的各个元素,这些 元素的数据类型完全相同 ,可以是内置的简单数据类型 (int、string 等),也可以是自定义的 struct 类型。

  • 固定长度:这意味着数组不可增长、不可缩减。想要扩展数组,只能创建新数组,将原数组的元素复制到新数组
  • 连续空间:这意味可以在缓存中保留的时间更长,搜索速度更快,是一种非常高效的数据结构,同时还意味着可以通过数值 index 的方式访问数组中的某个元素
  • 数据类型:意味着限制了每个 block 中可以存放什么样的数据,以及每个 block 可以存放多少字节的数据

例如,使用下面的语句声明一个长度为 4 的 int 类型的数组,那么这个数组最多只能存放 4 个元素,且所有元素都只能是 int 类型。同时,还为这个数组做了初始化。

arr_name := [4]int{3,5,22,12}

这个数组的结构如下图所示:
1.png

其中左上角的小格子中的数表示各元素所在数组中的位置,也就是它们对应的 index,index 从 0 开始计算。

声明、初始化和访问数组

因为 Go 中的数组要求数据类型固定、长度固定,所以在声明的时候需要给定长度和数据类型。

例如声明一个长度为 5、数据类型为 int 的数组,名为 arr_name。

var arr_name [5]int

必须注意,虽然我们称呼数组为 int 类型的数组,但 数组的数据类型是两部分组成的[n]TYPE,这个整体才是数组的数据类型 。所以,[5]int[6]int是两种不同的数组类型。不同数据类型,意味着如果数组赋值给另一数组时需要数据类型转换操作,而 Go 默认是不会进行数据类型转换的。

在 Go 中,当一个变量被声明之后,都会立即对其进行默认的赋 0 初始化。对 int 类型的变量会默认初始化为 0,对 string 类型的变量会初始化为空 "",对布尔类型的变量会初始化为 false,对指针 (引用) 类型的变量会初始化为 nil。

数组也是一种变量类型,也会被初始化。初始化的方式是数组中的所有元素都根据数据类型赋值 0。例如 int 类型的数组,元素全部赋值为 0,string 类型的数组,元素全部赋值为 "" 等。

所以,上面声明数组 arr_name 之后,它初始化后的结果如下:

2.png

可以直接输出数组:

import "fmt"
var new_arr [3]int
fmt.Println(new_arr) // 输出:[0 0 0]

可以将数组的声明和初始化为给定值的操作合并:

arr_name := [5]int{3,5,22,12,23}

如果将元素个数指定为特殊符号...,则表示通过初始化时的给定的值个数来推断数组长度:

// 声明长度为3的数组
arr_name1 := [...]int{2,3,4}

// 声明长度为4的数组
arr_name2 := [...]int{2,3,4,5}

如果声明数组时,只想给其中某几个元素初始化赋值,则使用索引号:

arr_name := [5]int{1:10, 2:20}

这表示声明长度为 5 的数组,但第 2 个元素的值为 10,第 3 个元素的值为 20,其它的元素 (第 1、4、5 个元素) 都默认初始化为 0。

这个数组声明后的结果如下:

3.png

要访问数组中的某个元素,可以使用索引:

arr_name := [5]int{2,3,4,5,6}

// 访问数组的第4个元素,将输出:5
print(arr_name[3])

// 修改数组第3个元素的值
arr_name[2] = 22

指针数组 (引用)

可以声明一个指针类型的数组,这样数组中就可以存放指针。注意,指针的默认初始化值为 nil。

例如,创建一个指向 int 类型的数组:

arr_name := [5]*int{1:new(int), 3:new(int)}

上面的*int表示数组只能存储*int类型的数据,也就是指向 int 的指针类型。new(TYPE)函数会为一个 TYPE 类型的数据结构划分内存并做默认初始化操作,并返回这个数据对象的指针,所以 new(int) 表示创建一个 int 类型的数据对象,同时返回指向这个对象的指针。

初始化后,它的结构如下:注意 int 指针指向的数据对象会被初始化为 0。

4.png

对数组中的指针元素进行赋值:

package main

import "fmt"

func main() {
	arr_name := [5]*int{1:new(int), 3:new(int)}
	*arr_name[1]=10
	*arr_name[3]=30
	
	// 赋值一个新元素
	arr_name[4]=new(int)
	
	fmt.Println(*arr_name[1])
	fmt.Println(*arr_name[3])
	fmt.Println(*arr_name[4])
}

5.png

数组拷贝

在 Go 中,由于数组算是一个值类型,所以可以将它赋值给其它数组。

因为数组类型的完整定义为[n]TYPE,所以数组赋值给其它数组的时候,n 和 TYPE 必须相同。

例如:

// 声明一个长度为5的string数组
var str_arr1 [5]string

// 声明并初始化另一个string数组
str_arr2 := [5]string{"Perl","Shell","Python","Go","Java"}

// 将str_arr2拷贝给str_arr1
str_arr1 = str_arr2

数组赋值给其它数组时,实际上是完整地拷贝一个数组。所以,如果数组是一个指针型的数组,那么拷贝的将是指针数组,而不会拷贝指针所指向的对象。

package main

import "fmt"

func main() {
	var str_arr1 [3]*string
	str_arr2 := [3]*string{
		new(string),
		new(string),
		new(string),
	}
	*str_arr2[0] = "Perl"
	*str_arr2[1] = "Python"
	*str_arr2[2] = "Shell"
	
	// 数组赋值,拷贝指针本身,而不拷贝指向的值
	str_arr1 = str_arr2
	
	// 将输出Python
	fmt.Println(*str_arr1[1])
}

拷贝后,它的结构如下:

6.png

array 遍历迭代

range 关键字可以对 array 进行迭代,每次返回一个 index 和对应的元素值。可以将 range 的迭代结合 for 循环对 array 进行遍历。

package main

func main() {
	my_arr := [4]int{11,22,33,44}
	for index,value := range my_arr {
		println("index:",index," , ","value",value)
	}
}

输出结果:

index: 0  ,  value 11
index: 1  ,  value 22
index: 2  ,  value 33
index: 3  ,  value 44

传递数组参数给函数

Go 中的传值方式是 按值传递 ,这意味着给变量赋值、给函数传参时,都是直接拷贝一个副本然后将副本赋值给对方的。这样的拷贝方式意味着:

  • 如果数据结构体积庞大,则要完整拷贝一个数据结构副本时效率会很低
  • 函数内部修改数据结构时,只能在函数内部生效,函数一退出就失效了,因为它修改的是副本对象

数组同样也遵循此规则。对于数组的赋值,上面数组拷贝中已经解释过了。如果函数的参数是数组类型,那么调用函数时传递给函数的数组也一样是这个数组拷贝后的一个副本。

例如,创建一个 100W 个元素的数组,将其传递给函数 foo():

var big_arr [1e6]int

func foo(a [1e6]int) {
	...
}

// 调用foo
foo(bigarr)

当上面声明 big_arr 后,就有 100W 个元素,假设这个 int 占用 8 字节,整个数组就占用 800W 字节,大约有 8M 数据。当调用 foo 的时候,Go 会直接复制这 8M 数据形成另一个数组副本,并将这个副本交给 foo 进行处理。在 foo 中处理的数组,实际上是这个副本,foo() 不会对原始的 big_arr 产生任何影响。

可以将数组的指针传递给函数,这样指针传递给函数时,复制给函数参数的是这个指针,总共才 8 个字节 (每个指针占用 1 个机器字长,64 位机器上是 64bit 共占用 8 字节),复制的数据量非常少。而且,因为复制的是指针,foo() 修改这个数组时,会直接影响原始数组。

var big_arr [1e6]int

// 生成数组的指针
ref_big_arr := &big_arr

func foo(ra *[1e6]int) {
	...
}

// 调用foo,传递指针
foo(ref_big_arr)

多维数组

可以通过组合两个一维数组的方式构成二维数组。一般在处理具有父、子关系或者有坐标关系的数据时,二维数组比较有用。

例如,声明二维数组:

var t_arr [4][2]int

这表示数组有 4 个元素,每个元素都是一个包含 2 元素的小数组。换一种方式,例如:

t_arr := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

还可以指定位置进行初始化:

t_arr := [4][2]int{1: {20, 21}, 3: {40, 41}}
t_arr := [4][2]int{1: {0: 20}, 3: {1: 41}}

7.png