Go语言数组用法详解(附带示例)
Go语言的数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
数组的长度是数组类型的一部分,所以不同长度或不同类型的元素组成的数组都是不同类型的数组。因此,Go语言中很少直接使用数组(因不同长度的数组属于不同的数组类型而无法直接赋值)。
与数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片工作原理还是要先理解数组。
第二种方式是在定义数组的时候顺序指定全部元素的初始值,数组的长度根据初始化元素的数目自动计算。
第三种方式是以索引的方式来初始化数组的元素,因此元素的初始值出现顺序比较随意。这种初始化方式和 map[int]Type 类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用零值初始化。
第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三个和第四个元素采用零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。
数组的内存结构比较简单。例如,下图给出的是数组 [4]int{2,3,5,7} 的内存结构:

图 1 数组[4]int{2,3,5,7}的内存结构
Go 语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式地指向第一个元素的指针(C语言的数组变量是指针),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。
如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。
其实数组指针类型除类型和数组不同之外,通过数组指针操作数组的方式和通过数组本身的操作类似,而且数组指针赋值时只会复制一个指针。但是数组指针类型依然不够灵活,因为数组的长度是数组类型的一部分,指向不同长度数组的数组指针类型也是完全不同的。
可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。内置函数 len() 可以用于计算数组的长度,cap() 函数可以用于计算数组的容量。不过对数组类型来说,len() 和 cap() 函数返回的结果始终是一样的,都是对应数组类型的长度。
for range 循环的性能可能会更好一些,因为这种循环可以保证不会出现数组越界的情形,在每次迭代对数组元素访问时可以省去对下标越界的判断。
使用 for range 循环遍历还可以忽略迭代时的下标:
我们还可以定义一个空的数组:
空数组虽然很少直接使用,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,如用于通道的同步操作:
当然,一般更倾向于用无类型的匿名结构体代替空数组:
我们可以用 fmt.Printf() 函数提供的 %T 或 %#v 谓词语法来打印数组的类型和详细信息:
数组的长度是数组类型的一部分,所以不同长度或不同类型的元素组成的数组都是不同类型的数组。因此,Go语言中很少直接使用数组(因不同长度的数组属于不同的数组类型而无法直接赋值)。
与数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片工作原理还是要先理解数组。
Go语言数组的定义
我们先看看数组有哪些定义方式: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 类型的初始化语法类似。数组的长度以出现的最大的索引为准,没有明确初始化的元素依然用零值初始化。
第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三个和第四个元素采用零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。
数组的内存结构比较简单。例如,下图给出的是数组 [4]int{2,3,5,7} 的内存结构:

图 1 数组[4]int{2,3,5,7}的内存结构
Go 语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式地指向第一个元素的指针(C语言的数组变量是指针),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。
如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。
var a = [...]int{1, 2, 3} // a是一个数组 var b = &a // b是指向数组的指针 fmt.Println(a[0], a[1]) // 打印数组的前两个元素 fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的写法和直接访问数组类似 for i, v := range b { // 通过数组指针遍历数组的元素 fmt.Println(i, v) }其中 b 是指向数组 a 的指针,但是通过 b 访问数组中元素的写法和直接访问 a 是类似的。还可以通过 for range 来遍历数组指针指向的数组元素。
其实数组指针类型除类型和数组不同之外,通过数组指针操作数组的方式和通过数组本身的操作类似,而且数组指针赋值时只会复制一个指针。但是数组指针类型依然不够灵活,因为数组的长度是数组类型的一部分,指向不同长度数组的数组指针类型也是完全不同的。
可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。内置函数 len() 可以用于计算数组的长度,cap() 函数可以用于计算数组的容量。不过对数组类型来说,len() 和 cap() 函数返回的结果始终是一样的,都是对应数组类型的长度。
Go语言数组的遍历
我们可以用 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 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
我们可以用 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}在 Go 语言中,数组类型是切片和字符串等结构的基础。以上对于数组的很多操作都可以直接用于字符串或切片中。