Skip to content

04|数组

数组是长度固定的同类型元素序列。Go 的数组和 C 语言类似,但有一个重要区别:数组是值类型。把数组赋给另一个变量、或把数组传给函数时,会复制整个数组的内容。这个特性在传递大数组时会导致明显的性能开销,也是理解切片(下一讲的内容)为什么存在的背景。

一、声明与初始化

数组声明时需要指定长度和元素类型:

go
package main

import "fmt"

func main() {
	var hosts [3]string
	hosts[0] = "web01"
	hosts[1] = "web02"
	hosts[2] = "db01"

	fmt.Println(hosts[0])     // web01
	fmt.Println(len(hosts))   // 3
}

声明时直接初始化:

go hosts

让编译器数元素个数:

go hosts

数组的长度是类型的一部分。[3]string[5]string 是两种不同的类型,不能互相赋值:

go
var a [3]string
var b [5]string
// a = b // 编译错误:cannot use b as [3]string

这个设计让编译器能在编译期就确定数组的内存布局,但也限制了数组的灵活性。

二、遍历

for 索引遍历:

go for
		fmt.Println(i, hosts[i])
	}

range 遍历(推荐):

go for
		fmt.Println(index, host)
	}

range 遍历数组时返回索引和元素的副本。修改副本不会影响原数组:

go for
		host = "new-" + host // 只修改了副本
	}
	fmt.Println(hosts) // 原数组不变

需要修改原数组时,用索引访问:

go for
		hosts[i] = "new-" + hosts[i]
	}

三、值类型特性

数组是值类型,赋值和传参都会复制整个数组:

go
func modifyHost(h [3]string) {
	h[0] = "proxy01" // 修改的是副本
}

func main() {
	hosts := [3]string{"web01", "web02", "db01"}
	modifyHost(hosts)
	fmt.Println(hosts[0]) // 仍然是 web01
}

modifyHost 收到的是 hosts 的完整副本,函数内部修改不影响原数组。大数组的复制开销明显——一个包含 1000 个结构体的数组,传参时会复制 1000 个结构体的全部字段。

如果需要让函数修改原数组,传数组指针:

go
func modifyHost(h *[3]string) {
	(*h)[0] = "proxy01"
}

func main() {
	hosts := [3]string{"web01", "web02", "db01"}
	modifyHost(&hosts)
	fmt.Println(hosts[0]) // proxy01
}

不过 Go 里更常见的做法是直接用切片(下一讲),而不是传递数组指针。

四、数组比较

元素类型可比较的数组,可以用 ==!= 直接比较:

go
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // true

数组比较要求长度相同、元素类型相同、每个对应位置的元素都相等。包含切片或映射的数组不能直接比较。

五、多维数组

Go 支持多维数组,常用于矩阵或表格数据:

go
var matrix [3][3]int
matrix[0][0] = 1
matrix[1][1] = 1
matrix[2][2] = 1

初始化:

go
matrix := [3][3]int{
	{1, 0, 0},
	{0, 1, 0},
	{0, 0, 1},
}

多维数组在内存中是连续存储的,matrix[0] 是一个 [3]int 数组,matrix[0][0] 是第一个元素。遍历多维数组用嵌套循环:

go for
		for j := 0; j < len(matrix[i]); j++ {
			fmt.Print(matrix[i][j], " ")
		}
		fmt.Println()
	}

六、数组与切片的区别

数组和切片看起来很像,但本质不同:

特性数组切片
长度固定,编译期确定可变,运行时动态增长
类型[N]T,长度是类型一部分[]T,长度不是类型一部分
传递方式值复制引用传递(复制的是底层指针+长度+容量)
使用场景长度固定且小,需要值语义长度不确定,需要动态增删

绝大多数场景下,切片比数组更灵活。数组主要用在两种地方:一是需要值语义的固定长度数据(如坐标点、颜色值),二是作为切片的底层存储。

七、常见错误

索引越界

go
hosts := [3]string{"web01"}
fmt.Println(hosts[5]) // panic: index out of bounds

数组索引从 0 开始,最大有效索引是 len-1。Go 会在运行时检查索引边界,越界访问会 panic。生产代码中,索引值来自外部输入时,先检查范围:

go if
		fmt.Println(hosts[index])
	}

用数组当函数参数处理大数据

go
func process(data [1000000]int) { // 复制了 100 万个整数
	// ...
}

大数组传参会复制大量数据,应该用切片或指针。除非数组很小(比如几个元素的坐标点),否则函数参数优先用切片。

把数组和切片混用

go
func printHosts(hosts []string) {
	for _, h := range hosts { fmt.Println(h) }
}

var hosts [3]string
// printHosts(hosts) // 编译错误:cannot use hosts as []string

数组不能自动转成切片。需要显式切片:printHosts(hosts[:])