数组

数组

在 Go 中,数组是值,是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。数组的大小是其类型的一部分,类型 [10]int[20]int 是不同的,不同长度或不同类型的数据组成的数组都是不同的类型。数组的内存结构比较简单。比如下面是一个 [4]int{2,3,5,7} 数组值对应的内存结构:

内存结构

将一个数组赋予另一个数组会复制其所有元素;若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。显而易见,这种复制的代价会较为昂贵。

数组定义

与切片(Slice)相比,数组(Array)用于表示固定长度的,相同类型的序列对象,可以使用如下形式创建:

[N]Type
[N]Type{value1, value2, ..., valueN}

// 由编译器自动计算数目
[...]Type{value1, value2, ..., valueN}

var a [3]int                    // 定义长度为3的int型数组, 元素全部为0
var b = [...]int{1, 2, 3}       // 定义长度为3的int型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2}    // 定义长度为3的int型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6
  • 第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。

  • 第二种方式定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。

  • 第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。这种初始化方式和 map[int]Type 类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用 0 值初始化。

  • 第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三第四个元素零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。

其具体使用方式为:

// 数组声明
var a [10]int

// 赋值
a[3] = 42

// 读取
i := a[3]

// 声明与初始化
var a = [2]int{1, 2}
a := [2]int{1, 2}

// 加 ... 会限制数组长度
a := [...]int{1, 2}

// 声明二维数组
array := [2][3]int{{1, 2, 3}, {4, 5, 6}}

数组不仅仅可以用于数值类型,还可以定义字符串数组、结构体数组、函数数组、接口数组、管道数组等等:

// 字符串数组
var s1 = [2]string{"hello", "world"}
var s2 = [...]string{"你好", "世界"}
var s3 = [...]string{1: "世界", 0: "你好", }

// 结构体数组
var line1 [2]image.Point
var line2 = [...]image.Point{image.Point{X: 0, Y: 0}, image.Point{X: 1, Y: 1}}
var line3 = [...]image.Point{{0, 0}, {1, 1}}

// 图像解码器数组
var decoder1 [2]func(io.Reader) (image.Image, error)
var decoder2 = [...]func(io.Reader) (image.Image, error){
	png.Decode,
	jpeg.Decode,
}

// 接口数组
var unknown1 [2]interface{}
var unknown2 = [...]interface{}{123, "你好"}

// 管道数组
var chanList = [2]chan int{}

空数组

var d [0]int       // 定义一个长度为0的数组
var e = [0]int{}   // 定义一个长度为0的数组
var f = [...]int{} // 定义一个长度为0的数组

长度为 0 的数组在内存中并不占用空间。空数组虽然很少直接使用,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,比如用于管道的同步操作:

c1 := make(chan [0]int)
go func() {
	fmt.Println("c1")
	c1 <- [0]int{}
}()
<-c1

在这里,我们并不关心管道中传输数据的真实类型,其中管道接收和发送操作只是用于消息的同步。对于这种场景,我们用空数组来作为管道类型可以减少管道元素赋值时的开销。当然一般更倾向于用无类型的匿名结构体代替:

c2 := make(chan struct{})
go func() {
	fmt.Println("c2")
	c2 <- struct{}{} // struct{}部分是类型, {}表示对应的结构体值
}()
<-c2

数组指针

Go 语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如 C 语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。

var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a                // b 是指向数组的指针

fmt.Println(a[0], a[1])   // 打印数组的前2个元素
fmt.Println(b[0], b[1])   // 通过数组指针访问数组元素的方式和数组类似

for i, v := range b {     // 通过数组指针迭代数组的元素
	fmt.Println(i, v)
}

其中 b 是指向 a 数组的指针,但是通过 b 访问数组中元素的写法和 a 类似的。还可以通过 for range 来迭代数组指针指向的数组元素。其实数组指针类型除了类型和数组不同之外,通过数组指针操作数组的方式和通过数组本身的操作类似,而且数组指针赋值时只会拷贝一个指针。但是数组指针类型依然不够灵活,因为数组的长度是数组类型的组成部分,指向不同长度数组的数组指针类型也是完全不同的。

数组属性与操作

Go 内置了 len 与 cap 函数,用于获取数组的尺寸与容量:

var arr = [3]int{1, 2, 3}
arr := [...]int{1, 2, 3}

len(arr) // 3
cap(arr) // 3

我们可以用 fmt.Printf 函数提供的 %T%#v 谓词语法来打印数组的类型和详细信息:

fmt.Printf("b: %T\n", b)  // b: [3]int
fmt.Printf("b: %#v\n", b) // b: [3]int{1, 2, 3}

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int

遍历

我们可以用 for 循环来迭代数组。下面常见的几种方式都可以用来遍历数组:

for i := range a {
	fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
	fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
	fmt.Printf("c[%d]: %d\n", i, c[i])
}

用 for range 方式迭代的性能可能会更好一些,因为这种迭代可以保证不会出现数组越界的情形,每轮迭代对数组元素的访问时可以省去对下标越界的判断。用 for range 方式迭代,还可以忽略迭代时的下标:

var times [5][0]int
for range times {
	fmt.Println("hello")
}

其中 times 对应一个[5][0]int 类型的数组,虽然第一维数组有长度,但是数组的元素[0]int 大小是 0,因此整个数组占用的内存大小依然是 0。没有付出额外的内存代价,我们就通过 for range 方式实现了 times 次快速迭代。

多维数组

Go 语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。声明多维数组的语法如下所示:

var array_name [size1][size2]...[sizen] array_type

其中,array_name 为数组的名字,array_type 为数组的类型,size1、size2 等等为数组每一维度的长度。二维数组是最简单的多维数组,二维数组本质上是由多个一维数组组成的。

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}

下图展示了上面示例中声明的二维数组在每次声明并初始化后包含的值。

上一页
下一页