数组是一种重要的数据结构,一旦声明长度就是固定的,实际中,更为灵活的切片可能使用更为广泛。切片底层实现就是使用的数组。

一 数组

数组是一种长度固定的数据结构,一旦声明,长度和元素的类型就不可变,并且在内存中是一段连续的块。数组中元素的个数即为数组的长度。数组的长度是一个int类型,可以使用内置函数len()来获取。另外,数组中的每一个元素都对应一个索引(下标),索引是一个整数,从0开始。对于一个数组a,其长度为len(a),可以使用索引0-len(a)-1来访问数组中的每一个元素。

数组长度不能为负,索引也不能为负

1.1 数组的声明

数组本身是一维类型的,但是可以将数组进行多层嵌套从而构造出多维数组,即数组的元素本身又是一个数组。

1.1.1 使用关键字var声明

var 数组名称 [长度]元素类型

示例:

var arr [3]int      // 声明一个长度为3,元素类型为int的数组
var x [16]byte      // 声明一个长度为16,元素类型为byte的数组
var p [2][2]float64 // 声明一个2x2的浮点二维数组,等价于[2]([2]float64)

数组在声明时,会使用每个元素对应的零值来初始化数组。对于整型数组,每个元素的初始值均为0。

1.1.2 使用数组字面量声明

数组名 := [长度]元素类型{元素0, 元素1, ..., 元素length-1}

示例:

arr := [4]int{1, 3, 4, 5} // 声明一个长度为4,且每个元素都初始化为具体的值

另外,还可以使用...代替长度,让Go根据初始化时的元素个数来确定其长度:

array := [...]float64{7.0, 8.5, 9.1} // 实际长度为3

由于数组的元素都有一个索引对应,所以也可以在声明时,指定特定元素的初始值:

// 指定索引为1,3,5的元素的初始值
// 初始化后的元素:[0 10 0 40 0 50 0 0]
arr := [8]int{1: 10, 3: 40, 5: 50}

1.2 数组的使用

1.2.1 访问数组

给定一个数组:

arr := [4]int{1, 3, 4, 5, 6}

前面提到,可以使用数组的索引来访问数组中的元素:
语法: 数组名[索引]

arr[0]      // 访问第0个元素
x := arr[1] // 将第1个元素赋值给其他变量
arr[2] = 10 // 修改第2个元素的值

另外,还可以使用数组[[索引开始]:[索引结束]]的形式来截取数组中的一部分元素,这样就形成了一个切片(参见下一小节):

a0 := arr      // 数组:直接把数组arr赋值给另一个数组,达到复制数组的目的
a1 := arr[1:3] // 切片:{3, 4}
a2 := arr[2:]  // 切片:等价于arr[2:4],{4, 5, 6}
a3 := arr[:3]  // 切片:等价于arr[0:3],{1, 3, 4}
a4 := arr[:]   // 切片:与arr 数组一样,{1, 3, 4, 5, 6}

其中,索引的开始和结束又称为上界和下界。

1.3 数组的值和引用

了解数组的工作方式很重要,尤其是在规划内存的详细布局时,可以帮助避免重新分配内存。
以下是几个关于Go数组的重要特点。

数组在Go和C中的工作方式有很大差异。在Go中:

  • 数组是一个值。将一个数组分配给另一个数组会复制所有元素。
  • 如果将数组传递给函数,它将接收数组的副本,而不是指向它的指针。
  • 数组的大小是其类型的一部分。类型[10]int[20]int是不同的。
在函数间传递数组时要小心复制问题,这可能不是你希望的

值属性很有用,但也很昂贵;如果你想要类似C的行为和效率,你可以传递一个指向数组的指针。

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // 注意显式的取址操作符&

但即便是这种风格也不是Go的习惯用法,应该使用切片代替。

二 切片

不同于数组,切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。切片包装了数组,为数据序列提供了更通用、更强大且方便的接口。除具有显示维度的条目(如转换矩阵)之外,Go中大多数数组编程都是使用切片而不是简单的数组完成的。

切片持有对底层数组的引用,如果将一个切片分配给另一个切片,则两者都引用同一个数组。如果函数使用切片参数,对切片元素所做的修改对调用者是可见的,类似于将指针传递给底层数组。

2.1 切片的声明

将数组声明中的长度去掉,就变成了切片声明:

var s []int

但是由于切片是引用类型,所以上述声明还未初始化,不能够使用,切片必须先初始化才能使用。切片的零值为nilnil切片的长度和容量为0且没有底层数组。类型[]T表示一个元素类型为T的切片。

可以借助内置函数make()来初始化切片:

make([]T, length)           // 只指定切片长度
make([]T, length, capacity) // 指定长度和容量

make函数会分配一个元素为零值的数组并返回一个引用了它的切片。其中,lengthcapacity均为整数,且length <= capacity

切片s的长度和容量满足:0 <= len(s) <= cap(s)

例如:

s := make([]int, 2, 5) // 创建一个长度为2,容量为5的整型切片

它等价于以下表达式:

s := new([5]int)[0:2]// 创建数组然后进行切片

需要注意的是,初始化好的切片只能访问长度指定的范围,其余超出长度但在容量范围内的元素并不能直接访问,还需要借助内置函数append来操作(见下文)。

另外,还可以使用切片字面量直接初始化:

s := []int{1,3,5,7} // 创建一个长度和容量均为4的整型切片

同样,也可以使用索引来初始化切片:

s := []string{5: "", 9: "hello"} // 创建一个长度和容量均为10的切片
注意声明切片和数组的区别,切片在[ ]中没有指定长度。

切片拥有 长度容量。切片的长度就是它所包含的元素个数。切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。

切片s的长度和容量可通过表达式len(s)cap(s)来获取。你可以通过重新切片来扩展一个切片,给它提供足够的容量,前提是,不能超过其容量。

2.1.1 二维切片

与数组一样,切片也是一维的,要创建二维或多维切片,需要定义一个切片的切片:

s := [][]byte     // 一个二维字节切片

多维切片内部切片的长度可以动态变化,且内部切片需要单独初始化。

2.2 切片表达式

切片表达式可以从切片、数组、字符串等构造字符串或切片,有两种变体:

2.2.1 简单切片表达式

对于字符串、数组、数组指针,或切片,可以通过2个下标来界定,即一个上界和一个下界,二者以冒号分隔:
a[low : high]
它会选择一个半开区间(前闭后开),包括第一个元素,但排除最后一个元素。

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4] // {2, 3, 4}

新的切片的长度为high - low,容量为底层数组的容量 - low
对于上述示例,新切片s的长度为4-1=3,容量为5-1=4。

以下表达式创建了一个切片,它包含primes中下标从 1 到 3 的元素:
s[1:4]

package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4]
    fmt.Println(s)
}

在进行切片时,可以利用它的默认行为来忽略上下界。切片下界的默认值为 0,上界则是该切片的长度。

对于数组
var a [10]int
来说,以下切片是等价的:

a[0:10]
a[:10]
a[0:]
a[:]

如果a是一个指向数组的指针,则a[low : high](*a)[low : high]的简写。对于数组或字符串,下标索引需要满足0 <= low <= high <= len(a),否则会引起索引越界。对于切片,它的上界是它的容量cap(a),而不是长度。

2.2.2 完整切片表达式

对于数组、数组指针,或非字符串切片,可以通过3个下标来界定:
a[low : high : max]

其中第三个下标max用于控制新切片的容量:max - low,只有第一个下标可以省略,默认为0:

a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
s := a[:3:5]

其他与简单切片表达式一样。如果a是一个指向数组的指针,则a[low : high : max](*a)[low : high : max]的简写。如果是一个数组,则它必须是可寻址的。索引需要满足0 <= low <= high <= max <= cap(a),否则会引起索引越界。数组的索引也是一样。

所有有关数组和切片的下标索引都是大于0的int类型。

2.2.3 切片就像数组的引用

切片并不存储任何数据,它只是描述了底层数组中的一段。基于这一点,在函数中传递切片与传递数组并不一样,切片本身非常小,因而成本也很低。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。

package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
}

2.3 向切片追加元素

为切片追加新的元素是种常用的操作,为此Go提供了内建的append函数。内建函数的文档对此函数有详细的介绍。

func append(s []T, vs ...T) []T

append的第一个参数s是一个元素类型为T的切片,其余类型为T的值将会追加到该切片的末尾。append的结果是一个包含原切片所有元素加上新添加元素的切片。

s的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

package main

import (
    "fmt"
)

func main() {
    s := make([]int, 2, 5)
    fmt.Println(s)
    s = append(s, 1, 2, 3, 4, 5)
    fmt.Println(s)
}

(要了解关于切片的更多内容,请阅读文章Go切片:用法和本质。)

三 数组和切片的遍历

for循环的range形式可遍历数组或切片。当使用for循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

可以将下标或值赋予 _ 来忽略它:
for i, _ := range pow
for _, value := range pow

若你只需要索引,忽略第二个变量即可:
for i := range pow

package main

import "fmt"

func main() {
    pow := make([]int, 10)
    for i := range pow {
        pow[i] = 1 << uint(i) // == 2**i
    }
    for _, value := range pow {
        fmt.Printf("%d\n", value)
    }
}

参考:
https://golang.org/doc/effective_go.html#arrays
https://golang.org/doc/effective_go.html#slices
https://golang.org/ref/spec#Array_types
https://golang.org/ref/spec#Slice_types
https://golang.org/ref/spec#Slice_expressions