7.1.1 概念

数组是具有相同 唯一类型 的一组以编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 [5] int 和 [10] int 是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的(如下)。

注意事项 如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型(参考 第 11 章)。当使用值时我们必须先做一个类型判断(参考 第 11 章)。

数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素vv者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。

声明的格式是:

  1. var identifier [len]type

例如:

  1. var arr1 [5]int

在内存中的结构是:7.1  声明和初始化 - 图1

每个元素是一个整型值,当声明数组时所有的元素都会被自动初始化为默认值 0。

arr1 的长度是 5,索引范围从 0 到 len(arr1)-1

第一个元素是 arr1[0],第三个元素是 arr1[2];总体来说索引 i 代表的元素是 arr1[i],最后一个元素是 arr1[len(arr1)-1]

对索引项为 i 的数组元素赋值可以这么操作:arr[i] = value,所以数组是 可变的

只有有效的索引可以被使用,当使用等于或者大于 len(arr1) 的索引时:如果编译器可以检测到,会给出索引超限的提示信息;如果检测不到的话编译会通过而运行时会 panic:(参考 第 13 章

  1. runtime error: index out of range

由于索引的存在,遍历数组的方法自然就是使用 for 结构:

  • 通过 for 初始化数组项
  • 通过 for 打印数组元素
  • 通过 for 依次处理元素

示例 7.1 for_arrays.go

  1. package main
  2. import "fmt"
  3. func main() {
  4. var arr1 [5]int
  5. for i:=0; i < len(arr1); i++ {
  6. arr1[i] = i * 2
  7. }
  8. for i:=0; i < len(arr1); i++ {
  9. fmt.Printf("Array at index %d is %d\n", i, arr1[i])
  10. }
  11. }

输出结果:

  1. Array at index 0 is 0
  2. Array at index 1 is 2
  3. Array at index 2 is 4
  4. Array at index 3 is 6
  5. Array at index 4 is 8

for 循环中的条件非常重要:i < len(arr1),如果写成 i <= len(arr1) 的话会产生越界错误。

IDIOM:

  1. for i:=0; i < len(arr1); i++{
  2. arr1[i] = ...
  3. }

也可以使用 for-range 的生成方式:

IDIOM:

  1. for i,_:= range arr1 {
  2. ...
  3. }

在这里 i 也是数组的索引。当然这两种 for 结构对于切片(slices)(参考 第 7 章)来说也同样适用。

问题 7.1 下面代码段的输出是什么?

  1. a := [...]string{"a", "b", "c", "d"}
  2. for i := range a {
  3. fmt.Println("Array item", i, "is", a[i])
  4. }

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)

那么这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2 的类型是 [5]int

这样的结果就是当把一个数组赋值给另一个时,需要再做一次数组内存的拷贝操作。例如:

  1. arr2 := *arr1
  2. arr2[2] = 100

这样两个数组就有了不同的值,在赋值后修改 arr2 不会对 arr1 生效。

所以在函数中数组作为参数传入时,如 func1(arr2),会产生一次数组拷贝,func1 方法不会修改原始的数组 arr2。

如果你想修改原数组,那么 arr2 必须通过 & 操作符以引用方式传过来,例如 func1 (&arr2),下面是一个例子

示例 7.2 pointer_array.go:

  1. package main
  2. import "fmt"
  3. func f(a [3]int) { fmt.Println(a) }
  4. func fp(a *[3]int) { fmt.Println(a) }
  5. func main() {
  6. var ar [3]int
  7. f(ar) // passes a copy of ar
  8. fp(&ar) // passes a pointer to ar
  9. }

输出结果:

  1. [0 0 0]
  2. &[0 0 0]

另一种方法就是生成数组切片并将其传递给函数(详见第 7.1.4 节)。

练习

练习 7.1:array_value.go: 证明当数组赋值时,发生了数组内存拷贝。

练习 7.2:for_array.go: 写一个循环并用下标给数组赋值(从 0 到 15)并且将数组打印在屏幕上。

练习 7.3:fibonacci_array.go: 在第 6.6 节我们看到了一个递归计算 Fibonacci 数值的方法。但是通过数组我们可以更快的计算出 Fibonacci 数。完成该方法并打印出前 50 个 Fibonacci 数字。

7.1.2 数组常量

如果数组值已经提前知道了,那么可以通过 数组常量 的方法来初始化数组,而不用依次使用 []= 方法(所有的组成元素都有相同的常量语法)。

示例 7.3 array_literals.go

  1. package main
  2. import "fmt"
  3. func main() {
  4. // var arrAge = [5]int{18, 20, 15, 22, 16}
  5. // var arrLazy = [...]int{5, 6, 7, 8, 22}
  6. // var arrLazy = []int{5, 6, 7, 8, 22}
  7. var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
  8. // var arrKeyValue = []string{3: "Chris", 4: "Ron"}
  9. for i:=0; i < len(arrKeyValue); i++ {
  10. fmt.Printf("Person at %d is %s\n", i, arrKeyValue[i])
  11. }
  12. }

第一种变化:

  1. var arrAge = [5]int{18, 20, 15, 22, 16}

注意 [5]int 可以从左边起开始忽略:[10]int {1, 2, 3} : 这是一个有 10 个元素的数组,除了前三个元素外其他元素都为 0。

第二种变化:

  1. var arrLazy = [...]int{5, 6, 7, 8, 22}

... 可同样可以忽略,从技术上说它们其实变化成了切片。

第三种变化:key: value syntax

  1. var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}

只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串,所以输出结果为:

  1. Person at 0 is
  2. Person at 1 is
  3. Person at 2 is
  4. Person at 3 is Chris
  5. Person at 4 is Ron

在这里数组长度同样可以写成 ... 或者直接忽略。

你可以取任意数组常量的地址来作为指向新实例的指针。

示例 7.4 pointer_array2.go

  1. package main
  2. import "fmt"
  3. func fp(a *[3]int) { fmt.Println(a) }
  4. func main() {
  5. for i := 0; i < 3; i++ {
  6. fp(&[3]int{i, i * i, i * i * i})
  7. }
  8. }

输出结果:

  1. &[0 0 0]
  2. &[1 1 1]
  3. &[2 4 8]

几何点(或者数学向量)是一个使用数组的经典例子。为了简化代码通常使用一个别名:

  1. type Vector3D [3]float32
  2. var vec Vector3D

7.1.3 多维数组

数组通常是一维的,但是可以用来组装成多维数组,例如:[3][5]int[2][2][2]float64

内部数组总是长度相同的。Go 语言的多维数组是矩形式的(唯一的例外是切片的数组,参见第 7.2.5 节)。

示例 7.5 multidim_array.go

  1. package main
  2. const (
  3. WIDTH = 1920
  4. HEIGHT = 1080
  5. )
  6. type pixel int
  7. var screen [WIDTH][HEIGHT]pixel
  8. func main() {
  9. for y := 0; y < HEIGHT; y++ {
  10. for x := 0; x < WIDTH; x++ {
  11. screen[x][y] = 0
  12. }
  13. }
  14. }

7.1.4 将数组传递给函数

把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:

  • 传递数组的指针
  • 使用数组的切片

接下来的例子阐明了第一种方法:

示例 7.6 array_sum.go

  1. package main
  2. import "fmt"
  3. func main() {
  4. array := [3]float64{7.0, 8.5, 9.1}
  5. x := Sum(&array) // Note the explicit address-of operator
  6. // to pass a pointer to the array
  7. fmt.Printf("The sum of the array is: %f", x)
  8. }
  9. func Sum(a *[3]float64) (sum float64) {
  10. for _, v := range *a { // derefencing *a to get back to the array is not necessary!
  11. sum += v
  12. }
  13. return
  14. }

输出结果:

  1. The sum of the array is: 24.600000

但这在 Go 中并不常用,通常使用切片(参考 第 7.2 节)。